From 791876aa827293f0e39502043e260342af48a68f Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Wed, 8 Mar 2023 09:28:57 +0200 Subject: [PATCH] Implemented cUrl handle caching (#877) * Implemented cUrl handle caching * Added Stringable polyfill * Removed temporary code for tests * Added component test to verify that agent opens only one connection * ci: release automation with GH and Buildkite (scaffolding) with manual dry-run (#873) * Made component test infrastructure shared by tests * Renamed report produced by component tests to fit expected pattern Renamed from build/phpunit-component-junit.xml to build/component-tests-phpunit-junit.xml * Implemented cUrl handle caching * Added Stringable polyfill * Removed temporary code for tests * Added component test to verify that agent opens only one connection * Made component test infrastructure shared by tests * Renamed report produced by component tests to fit expected pattern Renamed from build/phpunit-component-junit.xml to build/component-tests-phpunit-junit.xml * Fixed test report name * Fixed MockApmServer not cleaning test scoped data * Fixed MockApmServer status code * Fixed missing reset in MockApmServerHandle::cleanTestScoped * Minor log formatting improvement --------- Co-authored-by: Victor Martinez --- .ci/Jenkinsfile | 4 +- .ci/component-test.sh | 4 +- .ci/loop.sh | 2 +- .ci/validate_agent_installation.sh | 8 +- .github/workflows/test.yml | 4 +- docs/configuration.asciidoc | 6 +- phpcs.xml.dist | 1 + phpstan.neon | 1 + phpunit.xml.dist | 2 +- phpunit_component_tests.xml | 2 +- .../Impl/BackendComm/EventSender.php | 18 +- src/ElasticApm/Impl/Util/ArrayUtil.php | 17 +- src/ext/ConfigManager.c | 112 +++- src/ext/ConfigManager.h | 4 +- src/ext/ResultCode.c | 31 ++ src/ext/ResultCode.h | 48 +- src/ext/StringView.h | 20 +- src/ext/backend_comm.c | 313 +++++------ src/ext/backend_comm.h | 6 +- src/ext/basic_macros.h | 2 + src/ext/config.m4 | 1 + src/ext/config.w32 | 1 + src/ext/elastic_apm.c | 26 +- src/ext/elastic_apm_API.c | 14 +- src/ext/elastic_apm_API.h | 6 +- src/ext/lifecycle.c | 2 +- src/ext/time_util.c | 58 ++- src/ext/time_util.h | 40 +- src/ext/unit_tests/CMakeLists.txt | 27 +- src/ext/unit_tests/ResultCode_tests.c | 64 +++ src/ext/unit_tests/main.c | 4 + .../unit_tests/parse_value_with_units_tests.c | 493 ++++++++++++++++++ src/ext/unit_tests/time_util_tests.c | 43 ++ src/ext/unit_tests/unit_test_util.h | 11 +- src/ext/unit_tests/util_tests.c | 150 +++++- src/ext/util.c | 139 +++++ src/ext/util.h | 156 +++++- .../ComponentTests/ApiKeySecretTokenTest.php | 2 +- .../ComponentTests/BackendCommTest.php | 77 +++ .../CurlAutoInstrumentationTest.php | 3 +- .../ComponentTests/HttpTransactionTest.php | 1 + .../ComponentTests/UserAgentTest.php | 2 +- .../ComponentTests/Util/AppCodeHostBase.php | 5 + .../ComponentTests/Util/AppCodeHostHandle.php | 2 +- .../Util/BuiltinHttpServerAppCodeHost.php | 5 +- .../BuiltinHttpServerAppCodeHostStarter.php | 11 +- .../Util/CliScriptAppCodeHostHandle.php | 2 +- .../Util/ComponentTestCaseBase.php | 31 +- .../Util/ComponentTestsPhpUnitExtension.php | 11 + .../Util/DataFromAgentPlusRaw.php | 22 +- .../Util/DataFromAgentPlusRawAccumulator.php | 113 ++-- .../Util/DataFromAgentPlusRawExpectations.php | 109 ++-- .../Util/DataFromAgentPlusRawValidator.php | 10 +- .../ComponentTests/Util/GlobalTestInfra.php | 118 +++++ .../Util/HttpAppCodeHostHandle.php | 4 +- .../Util/HttpAppCodeRequestParams.php | 2 +- .../ComponentTests/Util/HttpServerHandle.php | 66 ++- .../ComponentTests/Util/HttpServerStarter.php | 53 +- .../ComponentTests/Util/InfraUtilForTests.php | 24 +- .../Util/IntakeApiConnection.php | 57 ++ .../ComponentTests/Util/IntakeApiRequest.php | 2 + .../Util/IntakeApiRequestDeserializer.php | 2 +- .../ComponentTests/Util/MockApmServer.php | 97 +++- .../Util/MockApmServerHandle.php | 63 ++- .../ComponentTests/Util/RawDataFromAgent.php | 73 +++ .../Util/RawDataFromAgentReceiverEvent.php | 66 +++ ...romAgentReceiverEventConnectionStarted.php | 44 ++ .../RawDataFromAgentReceiverEventRequest.php | 59 +++ ...FromAgentReceiverEventVisitorInterface.php | 30 ++ .../ComponentTests/Util/ResourcesCleaner.php | 132 ++++- .../Util/ResourcesCleanerHandle.php | 14 +- .../ComponentTests/Util/ResourcesClient.php | 12 +- .../Util/SpawnedProcessBase.php | 16 +- .../Util/SyslogClearerClient.php | 1 + .../ComponentTests/Util/TestCaseHandle.php | 90 ++-- .../ComponentTests/Util/TestInfraData.php | 2 + .../Util/TestInfraDataPerProcess.php | 4 +- .../Util/TestInfraHttpServerProcessBase.php | 107 ++-- .../Util/TestInfraHttpServerStarter.php | 12 +- .../Util/ArrayUtilForTests.php | 43 +- .../Util/AssertMessageBuilder.php | 83 +++ tests/ElasticApmTests/Util/DataFromAgent.php | 3 +- .../Util/DataFromAgentExpectations.php | 14 +- .../Util/DataFromAgentValidator.php | 148 +++++- .../JsonDeserializableTrait.php | 29 +- .../Deserialization/JsonSerializableTrait.php | 48 ++ tests/ElasticApmTests/Util/TraceActual.php | 30 ++ tests/ElasticApmTests/Util/TraceValidator.php | 26 +- tests/polyfills/Stringable.php | 49 ++ tests/polyfills/WeakMap.php | 1 + tests/polyfills/load.php | 1 + 91 files changed, 3150 insertions(+), 721 deletions(-) create mode 100644 src/ext/ResultCode.c create mode 100644 src/ext/unit_tests/ResultCode_tests.c create mode 100644 src/ext/unit_tests/parse_value_with_units_tests.c create mode 100644 tests/ElasticApmTests/ComponentTests/BackendCommTest.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/GlobalTestInfra.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/IntakeApiConnection.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgent.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEvent.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventConnectionStarted.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventRequest.php create mode 100644 tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventVisitorInterface.php create mode 100644 tests/ElasticApmTests/Util/AssertMessageBuilder.php create mode 100644 tests/ElasticApmTests/Util/Deserialization/JsonSerializableTrait.php create mode 100644 tests/polyfills/Stringable.php diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile index 2fdac4b9d..8ca763ace 100644 --- a/.ci/Jenkinsfile +++ b/.ci/Jenkinsfile @@ -143,7 +143,7 @@ pipeline { } post { always { - junit(allowEmptyResults: true, keepLongStdio: true, testResults: "${BASE_DIR}/build/*junit.xml,${BASE_DIR}/phpunit-junit.xml") + junit(allowEmptyResults: true, keepLongStdio: true, testResults: "${BASE_DIR}/build/*junit.xml") } } } @@ -559,7 +559,7 @@ def runTestingCommand(def args = [:]) { } } } finally { - junit(allowEmptyResults: true, keepLongStdio: true, testResults: "${BASE_DIR}/build/phpunit-*junit.xml") + junit(allowEmptyResults: true, keepLongStdio: true, testResults: "${BASE_DIR}/build/*junit.xml") } } diff --git a/.ci/component-test.sh b/.ci/component-test.sh index ccd4935a3..fabb3584b 100755 --- a/.ci/component-test.sh +++ b/.ci/component-test.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -xe +set -xe -o pipefail # Disable Elastic APM for any process outside the component tests to prevent noise in the logs export ELASTIC_APM_ENABLED=false @@ -27,6 +27,6 @@ else fi # Run component tests -mkdir -p /app/build/ +mkdir -p ./build/ composer run-script run_component_tests 2>&1 | tee /app/build/run_component_tests_output.txt diff --git a/.ci/loop.sh b/.ci/loop.sh index b3be526e2..385394d8e 100755 --- a/.ci/loop.sh +++ b/.ci/loop.sh @@ -6,7 +6,7 @@ PHP_VERSION=${3:-7.2} OUTPUT_FOLDER="build/loop-$DOCKERFILE-$PHP_VERSION" TEST_REPORT_TEST="junit.xml" -TEST_REPORT_COMPOSER="build/phpunit-component-junit.xml" +TEST_REPORT_COMPOSER="build/component-tests-phpunit-junit.xml" mkdir -p "${OUTPUT_FOLDER}" || true for (( c=1; c<=LOOPS; c++ )) diff --git a/.ci/validate_agent_installation.sh b/.ci/validate_agent_installation.sh index 9bb615697..30b303ce3 100755 --- a/.ci/validate_agent_installation.sh +++ b/.ci/validate_agent_installation.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -xe +set -xe -o pipefail function printInfoAboutEnvironment () { echo 'PHP version:' @@ -34,12 +34,18 @@ function runComponentTests () { run_command_with_timeout_and_retries_args=(--max-tries=3 "${run_command_with_timeout_and_retries_args[@]}") run_command_with_timeout_and_retries_args=(--increase-timeout-exponentially=yes "${run_command_with_timeout_and_retries_args[@]}") + mkdir -p ./build/ + set +e # shellcheck disable=SC2086 # composerCommand is not wrapped in quotes on purpose because it might contained multiple space separated strings .ci/run_command_with_timeout_and_retries.sh "${run_command_with_timeout_and_retries_args[@]}" -- "${composerCommand[@]}" local composerCommandExitCode=$? set -e + echo "Content of ./build/ begin" + ls -l ./build/ + echo "Content of ./build/ end" + echo "${composerCommand[*]} exited with an error code ${composerCommandExitCode}" if [ ${composerCommandExitCode} -eq 0 ] ; then local shouldPrintTheMostRecentSyslogFile=false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e60ea66b2..ab45fceff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -166,11 +166,11 @@ jobs: name: Prepare Upload run: >- find build - -name "phpunit-*junit.xml" + -name "*junit.xml" -exec bash -c 'mv {} "build/${ELASTIC_APM_PHP_TESTS_MATRIX_ROW}-$(basename {})"' \; - if: success() || failure() uses: actions/upload-artifact@v3 with: name: test-results - path: build/*-phpunit-*junit.xml + path: build/*junit.xml diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 6a44422ae..288bfc605 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -441,9 +441,9 @@ your communications using HTTPS. Unless you do so, your secret token could be ob If a request sending events to the APM server takes longer than the configured timeout, the request is canceled and the events are discarded. -This configuration option supports the duration suffixes: `ms`, `s` and `m`. -For example: `10s`. -This option's default unit is `s`, so `5` is interpreted as `5s`. +The value has to be provided in *<>*. + +This option's default unit is `s` (seconds). If the value is `0` (or `0ms`, `0s`, etc.) the timeout for sending events to the APM Server is disabled. diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 9eed2b550..817233826 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -3,5 +3,6 @@ + */polyfills/Stringable.php */polyfills/WeakMap.php diff --git a/phpstan.neon b/phpstan.neon index 53f2b5ac1..72356fe70 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,6 +8,7 @@ parameters: reportUnmatchedIgnoredErrors: false excludePaths: + - tests/polyfills/Stringable.php - tests/polyfills/WeakMap.php ignoreErrors: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8ec679af4..a1a5c38ec 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -26,7 +26,7 @@ - + diff --git a/phpunit_component_tests.xml b/phpunit_component_tests.xml index 82d069028..96709ad64 100644 --- a/phpunit_component_tests.xml +++ b/phpunit_component_tests.xml @@ -26,7 +26,7 @@ - + diff --git a/src/ElasticApm/Impl/BackendComm/EventSender.php b/src/ElasticApm/Impl/BackendComm/EventSender.php index 769490fdc..2c82ad0fe 100644 --- a/src/ElasticApm/Impl/BackendComm/EventSender.php +++ b/src/ElasticApm/Impl/BackendComm/EventSender.php @@ -152,10 +152,8 @@ function (MetricSet $metricSet) use (&$serializedEvents) { && $loggerProxy->log( 'Calling elastic_apm_send_to_server...', [ - 'disableSend' => $this->config->disableSend(), - 'serverTimeout' => $this->config->serverTimeout(), - 'strlen(userAgentHttpHeader)' => strlen($this->userAgentHttpHeader), - 'strlen(serializedEvents)' => strlen($serializedEvents), + 'userAgentHttpHeader' => $this->userAgentHttpHeader, + 'strlen(serializedEvents)' => strlen($serializedEvents), ] ); @@ -165,17 +163,7 @@ function (MetricSet $metricSet) use (&$serializedEvents) { * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection * @phpstan-ignore-next-line */ - \elastic_apm_send_to_server( - self::boolToInt($this->config->disableSend()), - $this->config->serverTimeout(), - $this->userAgentHttpHeader, - $serializedEvents - ); + \elastic_apm_send_to_server($this->userAgentHttpHeader, $serializedEvents); } } - - private static function boolToInt(bool $boolVal): int - { - return $boolVal ? 1 : 0; - } } diff --git a/src/ElasticApm/Impl/Util/ArrayUtil.php b/src/ElasticApm/Impl/Util/ArrayUtil.php index b35498434..6fbd637b9 100644 --- a/src/ElasticApm/Impl/Util/ArrayUtil.php +++ b/src/ElasticApm/Impl/Util/ArrayUtil.php @@ -159,17 +159,16 @@ public static function getNullableIntValueIfKeyExistsElse($key, array $array, ?i } /** - * @param string $key - * @param mixed $defaultValue - * @param array $array - * @return mixed + * @template TKey of string|int + * @template TValue * - * @template T - * @phpstan-param T $defaultValue - * @phpstan-param T[] $array - * @phpstan-return T + * @param TKey $key + * @param TValue $defaultValue + * @param array $array + * + * @return TValue */ - public static function &getOrAdd(string $key, $defaultValue, array $array) + public static function &getOrAdd($key, $defaultValue, array &$array) { if (!array_key_exists($key, $array)) { $array[$key] = $defaultValue; diff --git a/src/ext/ConfigManager.c b/src/ext/ConfigManager.c index 4e7e5c009..62c98b2eb 100644 --- a/src/ext/ConfigManager.c +++ b/src/ext/ConfigManager.c @@ -45,6 +45,7 @@ enum ParsedOptionValueType parsedOptionValueType_string, parsedOptionValueType_int, parsedOptionValueType_duration, + parsedOptionValueType_size, end_parsedOptionValueType }; @@ -64,6 +65,7 @@ struct ParsedOptionValue String stringValue; int intValue; Duration durationValue; + Size sizeValue; } u; }; typedef struct ParsedOptionValue ParsedOptionValue; @@ -82,13 +84,21 @@ typedef struct EnumOptionAdditionalMetadata EnumOptionAdditionalMetadata; struct DurationOptionAdditionalMetadata { DurationUnits defaultUnits; + bool isNegativeValid; }; typedef struct DurationOptionAdditionalMetadata DurationOptionAdditionalMetadata; +struct SizeOptionAdditionalMetadata +{ + SizeUnits defaultUnits; +}; +typedef struct SizeOptionAdditionalMetadata SizeOptionAdditionalMetadata; + union OptionAdditionalMetadata { EnumOptionAdditionalMetadata enumData; DurationOptionAdditionalMetadata durationData; + SizeOptionAdditionalMetadata sizeData; }; typedef union OptionAdditionalMetadata OptionAdditionalMetadata; @@ -343,7 +353,15 @@ static ResultCode parseDurationValue( const OptionMetadata* optMeta, String rawV ResultCode parseResultCode = parseDuration( stringToView( rawValue ) , optMeta->additionalData.durationData.defaultUnits , /* out */ &parsedValue->u.durationValue ); - if ( parseResultCode == resultSuccess ) parsedValue->type = parsedOptionValueType_duration; + if ( parseResultCode == resultSuccess ) + { + if ( parsedValue->u.durationValue.valueInUnits < 0 && ! optMeta->additionalData.durationData.isNegativeValid ) + { + return resultParsingFailed; + } + parsedValue->type = parsedOptionValueType_duration; + } + return parseResultCode; } @@ -368,6 +386,42 @@ static void parsedDurationValueToZval( const OptionMetadata* optMeta, ParsedOpti RETURN_DOUBLE( durationToMilliseconds( parsedValue.u.durationValue ) ); } +static ResultCode parseSizeValue( const OptionMetadata* optMeta, String rawValue, /* out */ ParsedOptionValue* parsedValue ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( optMeta ); + ELASTIC_APM_ASSERT_EQ_UINT64( optMeta->defaultValue.type, parsedOptionValueType_size ); + ELASTIC_APM_ASSERT_VALID_PTR( rawValue ); + ELASTIC_APM_ASSERT_VALID_PTR( parsedValue ); + ELASTIC_APM_ASSERT_EQ_UINT64( parsedValue->type, parsedOptionValueType_undefined ); + + ResultCode parseResultCode = parseSize( stringToView( rawValue ) + , optMeta->additionalData.sizeData.defaultUnits + , /* out */ &parsedValue->u.sizeValue ); + if ( parseResultCode == resultSuccess ) parsedValue->type = parsedOptionValueType_size; + return parseResultCode; +} + +static String streamParsedSize( const OptionMetadata* optMeta, ParsedOptionValue parsedValue, TextOutputStream* txtOutStream ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( optMeta ); + ELASTIC_APM_ASSERT_EQ_UINT64( optMeta->defaultValue.type, parsedOptionValueType_size ); + ELASTIC_APM_ASSERT_VALID_PARSED_OPTION_VALUE( parsedValue ); + ELASTIC_APM_ASSERT_EQ_UINT64( parsedValue.type, optMeta->defaultValue.type ); + + return streamSize( parsedValue.u.sizeValue, txtOutStream ); +} + +static void parsedSizeValueToZval( const OptionMetadata* optMeta, ParsedOptionValue parsedValue, zval* return_value ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( optMeta ); + ELASTIC_APM_ASSERT_EQ_UINT64( optMeta->defaultValue.type, parsedOptionValueType_size ); + ELASTIC_APM_ASSERT_VALID_PARSED_OPTION_VALUE( parsedValue ); + ELASTIC_APM_ASSERT_EQ_UINT64( parsedValue.type, optMeta->defaultValue.type ); + ELASTIC_APM_ASSERT_VALID_PTR( return_value ); + + RETURN_DOUBLE( sizeToBytes( parsedValue.u.sizeValue ) ); +} + static ResultCode parseEnumValue( const OptionMetadata* optMeta, String rawValue, /* out */ ParsedOptionValue* parsedValue ) { @@ -559,7 +613,7 @@ static OptionMetadata buildDurationOptionMetadata( , SetConfigSnapshotFieldFunc setFieldFunc , GetConfigSnapshotFieldFunc getFieldFunc , DurationUnits defaultUnits -) + , bool isNegativeValid ) { return (OptionMetadata) { @@ -575,7 +629,36 @@ static OptionMetadata buildDurationOptionMetadata( .setField = setFieldFunc, .getField = getFieldFunc, .parsedValueToZval = &parsedDurationValueToZval, - .additionalData = (OptionAdditionalMetadata){ .durationData = (DurationOptionAdditionalMetadata){ .defaultUnits = defaultUnits } } + .additionalData = (OptionAdditionalMetadata){ .durationData = (DurationOptionAdditionalMetadata){ .defaultUnits = defaultUnits, .isNegativeValid = isNegativeValid } } + }; +} + +static OptionMetadata buildSizeOptionMetadata( + String name + , StringView iniName + , bool isSecret + , bool isDynamic + , Size defaultValue + , SetConfigSnapshotFieldFunc setFieldFunc + , GetConfigSnapshotFieldFunc getFieldFunc + , SizeUnits defaultUnits +) +{ + return (OptionMetadata) + { + .name = name, + .iniName = iniName, + .isSecret = isSecret, + .isDynamic = isDynamic, + .isLoggingRelated = false, + .defaultValue = { .type = parsedOptionValueType_size, .u.sizeValue = defaultValue }, + .interpretIniRawValue = &interpretStringIniRawValue, + .parseRawValue = &parseSizeValue, + .streamParsedValue = &streamParsedSize, + .setField = setFieldFunc, + .getField = getFieldFunc, + .parsedValueToZval = &parsedSizeValueToZval, + .additionalData = (OptionAdditionalMetadata){ .sizeData = (SizeOptionAdditionalMetadata){ .defaultUnits = defaultUnits } } }; } @@ -686,7 +769,7 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, breakdownMetrics ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, captureErrors ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, devInternal ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, disableInstrumentations ) -ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, disableSend ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, disableSend ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, enabled ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, environment ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, hostname ) @@ -710,7 +793,7 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, profilingInferredSpansMinDur ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, profilingInferredSpansSamplingInterval ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, sanitizeFieldNames ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, secretToken ) -ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serverTimeout ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( durationValue, serverTimeout ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serverUrl ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceName ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceNodeName ) @@ -746,8 +829,8 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, verifyServerCert ) #define ELASTIC_APM_INIT_METADATA( buildFunc, fieldName, optName, defaultValue ) \ ELASTIC_APM_INIT_METADATA_EX( buildFunc, fieldName, optName, /* isSecret */ false, /* isDynamic */ false, defaultValue ) -#define ELASTIC_APM_INIT_DURATION_METADATA( fieldName, optName, defaultValue, defaultUnits ) \ - ELASTIC_APM_INIT_METADATA_EX( buildDurationOptionMetadata, fieldName, optName, /* isSecret */ false, /* isDynamic */ false, defaultValue, defaultUnits ) +#define ELASTIC_APM_INIT_DURATION_METADATA( fieldName, optName, defaultValue, defaultUnits, isNegativeValid ) \ + ELASTIC_APM_INIT_METADATA_EX( buildDurationOptionMetadata, fieldName, optName, /* isSecret */ false, /* isDynamic */ false, defaultValue, defaultUnits, isNegativeValid ) #define ELASTIC_APM_INIT_SECRET_METADATA( buildFunc, fieldName, optName, defaultValue ) \ ELASTIC_APM_INIT_METADATA_EX( buildFunc, fieldName, optName, /* isSecret */ true, /* isDynamic */ false, defaultValue ) @@ -871,10 +954,10 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) /* defaultValue: */ NULL ); ELASTIC_APM_INIT_METADATA( - buildStringOptionMetadata, + buildBoolOptionMetadata, disableSend, ELASTIC_APM_CFG_OPT_NAME_DISABLE_SEND, - /* defaultValue: */ NULL ); + /* defaultValue: */ false ); ELASTIC_APM_INIT_METADATA( buildBoolOptionMetadata, @@ -974,11 +1057,12 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) ELASTIC_APM_CFG_OPT_NAME_SECRET_TOKEN, /* defaultValue: */ NULL ); - ELASTIC_APM_INIT_METADATA( - buildStringOptionMetadata, - serverTimeout, - ELASTIC_APM_CFG_OPT_NAME_SERVER_TIMEOUT, - /* defaultValue: */ NULL ); + ELASTIC_APM_INIT_DURATION_METADATA( + serverTimeout + , ELASTIC_APM_CFG_OPT_NAME_SERVER_TIMEOUT + , /* defaultValue */ makeDuration( 30, durationUnits_second ) + , /* defaultUnits: */ durationUnits_second + , /* isNegativeValid */ false ); ELASTIC_APM_INIT_METADATA( buildStringOptionMetadata, diff --git a/src/ext/ConfigManager.h b/src/ext/ConfigManager.h index 3039b0153..9b39f393c 100644 --- a/src/ext/ConfigManager.h +++ b/src/ext/ConfigManager.h @@ -151,7 +151,7 @@ struct ConfigSnapshot bool captureErrors; String devInternal; String disableInstrumentations; - String disableSend; + bool disableSend; bool enabled; String environment; String hostname; @@ -176,7 +176,7 @@ struct ConfigSnapshot String sanitizeFieldNames; String secretToken; String serverUrl; - String serverTimeout; + Duration serverTimeout; String serviceName; String serviceNodeName; String serviceVersion; diff --git a/src/ext/ResultCode.c b/src/ext/ResultCode.c new file mode 100644 index 000000000..656170de3 --- /dev/null +++ b/src/ext/ResultCode.c @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "ResultCode.h" +#include "basic_macros.h" + +StringView resultCodeNames[ numberOfResultCodes ] = +{ + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( resultSuccess ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( resultOutOfMemory ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( resultParsingFailed ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( resultCurlFailure ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( resultSyncObjUseAfterFork ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( resultFailure ), +}; diff --git a/src/ext/ResultCode.h b/src/ext/ResultCode.h index eba08a3a9..8d689320a 100644 --- a/src/ext/ResultCode.h +++ b/src/ext/ResultCode.h @@ -21,43 +21,39 @@ #include #include "basic_types.h" +#include "StringView.h" enum ResultCode { resultSuccess, resultOutOfMemory, - resultInvalidFormat, + resultParsingFailed, resultCurlFailure, resultSyncObjUseAfterFork, - resultFailure + resultFailure, + + numberOfResultCodes }; typedef enum ResultCode ResultCode; -static inline String resultCodeToString( ResultCode resultCode ) -{ - switch ( resultCode ) - { - case resultSuccess: - return "resultSuccess"; - - case resultOutOfMemory: - return "resultOutOfMemory"; +extern StringView resultCodeNames[ numberOfResultCodes ]; - case resultInvalidFormat: - return "resultInvalidFormat"; - - case resultCurlFailure: - return "resultCurlFailure"; - - case resultSyncObjUseAfterFork: - return "resultSyncObjUseAfterFork"; +static inline +bool isValidResultCode( ResultCode resultCode ) +{ + return ( resultSuccess <= resultCode ) && ( resultCode < numberOfResultCodes ); +} - case resultFailure: - return "resultFailure"; +#define ELASTIC_APM_UNKNOWN_RESULT_CODE_AS_STRING "" - default: - return "UNKNOWN"; +static inline +String resultCodeToString( ResultCode resultCode ) +{ + if ( isValidResultCode( resultCode ) ) + { + return resultCodeNames[ resultCode ].begin; } + return ELASTIC_APM_UNKNOWN_RESULT_CODE_AS_STRING; } #define ELASTIC_APM_CALL_IF_FAILED_GOTO( expr ) \ @@ -73,3 +69,9 @@ static inline String resultCodeToString( ResultCode resultCode ) } while ( 0 ) #define ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE() ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultFailure ) + +#define ELASTIC_APM_CALL_EARLY_GOTO_FINALLY_WITH_SUCCESS() \ + do { \ + resultCode = resultSuccess; \ + goto finally; \ + } while ( 0 ) diff --git a/src/ext/StringView.h b/src/ext/StringView.h index f8b04978f..631b2f25c 100644 --- a/src/ext/StringView.h +++ b/src/ext/StringView.h @@ -86,22 +86,22 @@ const char* stringViewEnd( StringView strView ) return strView.begin + strView.length; } +#define ELASTIC_APM_STRING_LITERAL_TO_VIEW( stringLiteral ) ((StringView){ .begin = (stringLiteral), .length = (sizeof(stringLiteral) - 1) }) + static inline -StringView makeStringViewFromLiteralHelper( const char* begin, size_t size ) +StringView makeStringViewFromString( String zeroTermStr ) { - ELASTIC_APM_ASSERT_VALID_PTR( begin ); - ELASTIC_APM_ASSERT_GE_UINT64( size, 1 ); - ELASTIC_APM_ASSERT_EQ_CHAR( begin[ size - 1 ], '\0' ); + ELASTIC_APM_ASSERT_VALID_PTR( zeroTermStr ); - return makeStringView( begin, /* length: */ size - 1 ); + return makeStringView( zeroTermStr, /* length: */ strlen( zeroTermStr ) ); } -#define ELASTIC_APM_STRING_LITERAL_TO_VIEW( stringLiteral ) ( makeStringViewFromLiteralHelper( (stringLiteral), sizeof( (stringLiteral) ) ) ) - static inline -StringView makeStringViewFromString( String zeroTermStr ) +StringView subStringView( StringView inStrVw, size_t offset ) { - ELASTIC_APM_ASSERT_VALID_PTR( zeroTermStr ); + ELASTIC_APM_ASSERT_VALID_STRING_VIEW( inStrVw ); - return makeStringView( zeroTermStr, /* length: */ strlen( zeroTermStr ) ); + return inStrVw.length >= offset + ? makeStringView( inStrVw.begin + offset, inStrVw.length - offset ) + : makeEmptyStringView(); } diff --git a/src/ext/backend_comm.c b/src/ext/backend_comm.c index d75247a99..faef631a4 100644 --- a/src/ext/backend_comm.c +++ b/src/ext/backend_comm.c @@ -107,9 +107,9 @@ size_t logResponse( void* data, size_t unusedSizeParam, size_t dataSize, void* u return dataSize; } -#define ELASTIC_APM_CURL_EASY_SETOPT( curl, curlOptionId, ... ) \ +#define ELASTIC_APM_CURL_EASY_SETOPT( curlHandle, curlOptionId, ... ) \ do { \ - CURLcode curl_easy_setopt_ret_val = curl_easy_setopt( curl, curlOptionId, __VA_ARGS__ ); \ + CURLcode curl_easy_setopt_ret_val = curl_easy_setopt( curlHandle, curlOptionId, __VA_ARGS__ ); \ if ( curl_easy_setopt_ret_val != CURLE_OK ) \ { \ ELASTIC_APM_LOG_ERROR( "Failed to set cUrl option. curlOptionId: %d.", curlOptionId ); \ @@ -127,86 +127,88 @@ ResultCode addToCurlStringList( /* in,out */ struct curl_slist** pList, const ch if ( newList == NULL ) { ELASTIC_APM_LOG_ERROR( "Failed to curl_slist_append(); strToAdd: %s", strToAdd ); - return resultFailure; + return resultCurlFailure; } *pList = newList; return resultSuccess; } -ResultCode syncSendEventsToApmServer( bool disableSend - , double serverTimeoutMilliseconds - , const ConfigSnapshot* config - , String userAgentHttpHeader - , StringView serializedEvents ) +struct ConnectionData { - long serverTimeoutMillisecondsLong = (long) ceil( serverTimeoutMilliseconds ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( - "Sending events to APM Server..." - "; config->serverUrl: %s" - "; disableSend: %s" - "; serverTimeoutMilliseconds: %f (as integer: %"PRIu64")" - "; userAgentHttpHeader: `%s'" - "; serializedEvents [length: %"PRIu64"]:\n%.*s" - , config->serverUrl - , boolToString( disableSend ) - , serverTimeoutMilliseconds, (UInt64) serverTimeoutMillisecondsLong - , userAgentHttpHeader - , (UInt64) serializedEvents.length, (int) serializedEvents.length, serializedEvents.begin ); + CURL* curlHandle; + struct curl_slist* requestHeaders; +}; +typedef struct ConnectionData ConnectionData; +ConnectionData g_connectionData = { .curlHandle = NULL, .requestHeaders = NULL }; - if ( disableSend ) +void cleanupConnectionData( ConnectionData* connectionData ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( connectionData ); + + if ( connectionData->requestHeaders != NULL ) { - ELASTIC_APM_LOG_DEBUG( "disable_send (disableSend) configuration option is set to true - discarding events instead of sending" ); - return resultSuccess; + curl_slist_free_all( connectionData->requestHeaders ); + connectionData->requestHeaders = NULL; } - ResultCode resultCode; - CURL* curl = NULL; - CURLcode result; - enum + if ( connectionData->curlHandle != NULL ) { - authBufferSize = 256 - }; + curl_easy_cleanup( connectionData->curlHandle ); + connectionData->curlHandle = NULL; + } +} + +ResultCode initConnectionData( const ConfigSnapshot* config, ConnectionData* connectionData, StringView userAgentHttpHeader ) +{ + ResultCode resultCode; + enum { authBufferSize = 256 }; char auth[authBufferSize]; - enum - { - urlBufferSize = 256 - }; - char url[urlBufferSize]; - struct curl_slist* requestHeaders = NULL; - int snprintfRetVal; const char* authKind = NULL; const char* authValue = NULL; + int snprintfRetVal; + 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_VALID_PTR( connectionData ); + ELASTIC_APM_ASSERT( connectionData->curlHandle == NULL, "" ); + ELASTIC_APM_ASSERT( connectionData->requestHeaders == NULL, "" ); - /* get a curl handle */ - curl = curl_easy_init(); - if ( curl == NULL ) + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( + "config: { serverUrl: %s, disableSend: %s, serverTimeout: %s }" + "; userAgentHttpHeader: `%s'" + , config->serverUrl + , boolToString( config->disableSend ) + , streamDuration( config->serverTimeout, &txtOutStream ) + , streamStringView( userAgentHttpHeader, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + + connectionData->curlHandle = curl_easy_init(); + if ( connectionData->curlHandle == NULL ) { ELASTIC_APM_LOG_ERROR( "curl_easy_init() returned NULL" ); - ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultCurlFailure ); } - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_POST, 1L ); - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_POSTFIELDS, serializedEvents.begin ); - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_POSTFIELDSIZE, serializedEvents.length ); - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_WRITEFUNCTION, logResponse ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_WRITEFUNCTION, logResponse ); - if ( serverTimeoutMillisecondsLong == 0 ) + if ( config->serverTimeout.valueInUnits == 0 ) { - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( - "Timeout is disabled. serverTimeoutMilliseconds: %f (as integer: %"PRIu64")" - , serverTimeoutMilliseconds, (UInt64) serverTimeoutMillisecondsLong ); + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "Timeout is disabled. %s (serverTimeout): %s" + , ELASTIC_APM_CFG_OPT_NAME_SERVER_TIMEOUT, streamDuration( config->serverTimeout, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); } else { - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_TIMEOUT_MS, serverTimeoutMillisecondsLong ); + long serverTimeoutInMilliseconds = (long)durationToMilliseconds( config->serverTimeout ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_TIMEOUT_MS, serverTimeoutInMilliseconds ); } if ( ! config->verifyServerCert ) { ELASTIC_APM_LOG_DEBUG( "verify_server_cert configuration option is set to false" " - disabling SSL/TLS certificate verification for communication with APM Server..." ); - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_SSL_VERIFYPEER, 0L ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_SSL_VERIFYPEER, 0L ); } // Authorization with API key or secret token if present @@ -231,23 +233,49 @@ ResultCode syncSendEventsToApmServer( bool disableSend ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } ELASTIC_APM_LOG_TRACE( "Adding header: %s", auth ); - ELASTIC_APM_CALL_IF_FAILED_GOTO( addToCurlStringList( /* in,out */ &requestHeaders, auth ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( addToCurlStringList( /* in,out */ &connectionData->requestHeaders, auth ) ); } - ELASTIC_APM_CALL_IF_FAILED_GOTO( addToCurlStringList( /* in,out */ &requestHeaders, "Content-Type: application/x-ndjson" ) ); - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_HTTPHEADER, requestHeaders ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( addToCurlStringList( /* in,out */ &connectionData->requestHeaders, "Content-Type: application/x-ndjson" ) ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_HTTPHEADER, connectionData->requestHeaders ); - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_USERAGENT, userAgentHttpHeader ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_USERAGENT, userAgentHttpHeader ); + + resultCode = resultSuccess; + finally: + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); + return resultCode; + + failure: + goto finally; +} + +ResultCode syncSendEventsToApmServerWithConn( const ConfigSnapshot* config, ConnectionData* connectionData, StringView serializedEvents ) +{ + ResultCode resultCode; + CURLcode curlResult; + enum { urlBufferSize = 256 }; + char url[urlBufferSize]; + int snprintfRetVal; + + ELASTIC_APM_ASSERT_VALID_PTR( connectionData ); + ELASTIC_APM_ASSERT( connectionData->curlHandle != NULL, "" ); + + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); + + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_POST, 1L ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_POSTFIELDS, serializedEvents.begin ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_POSTFIELDSIZE, serializedEvents.length ); snprintfRetVal = snprintf( url, urlBufferSize, "%s/intake/v2/events", config->serverUrl ); - if ( snprintfRetVal < 0 || snprintfRetVal >= authBufferSize ) + if ( snprintfRetVal < 0 || snprintfRetVal >= urlBufferSize ) { ELASTIC_APM_LOG_ERROR( "Failed to build full URL to APM Server's intake API. snprintfRetVal: %d", snprintfRetVal ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } - ELASTIC_APM_CURL_EASY_SETOPT( curl, CURLOPT_URL, url ); + ELASTIC_APM_CURL_EASY_SETOPT( connectionData->curlHandle, CURLOPT_URL, url ); - result = curl_easy_perform( curl ); - if ( result != CURLE_OK ) + curlResult = curl_easy_perform( connectionData->curlHandle ); + if ( curlResult != CURLE_OK ) { char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE ]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); @@ -257,35 +285,65 @@ ResultCode syncSendEventsToApmServer( bool disableSend " Error message: `%s'." " Current process command line: `%s'" , url - , curl_easy_strerror( result ) + , curl_easy_strerror( curlResult ) , streamCurrentProcessCommandLine( &txtOutStream ) ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } long responseCode; - curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &responseCode ); + curl_easy_getinfo( connectionData->curlHandle, CURLINFO_RESPONSE_CODE, &responseCode ); ELASTIC_APM_LOG_DEBUG( "Sent events to APM Server. Response HTTP code: %ld. URL: `%s'.", responseCode, url ); resultCode = resultSuccess; - finally: + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); + return resultCode; + + failure: + goto finally; +} + +ResultCode syncSendEventsToApmServer( const ConfigSnapshot* config, StringView userAgentHttpHeader, StringView serializedEvents ) +{ + char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE ]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + ResultCode resultCode; + ConnectionData* connectionData = &g_connectionData; + + ELASTIC_APM_ASSERT_VALID_PTR( connectionData ); + + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( + "Sending events to APM Server..." + "; config: { serverUrl: %s, disableSend: %s, serverTimeout: %s }" + "; userAgentHttpHeader: `%s'" + "; serializedEvents [length: %"PRIu64"]:\n%.*s" + , config->serverUrl + , boolToString( config->disableSend ) + , streamDuration( config->serverTimeout, &txtOutStream ) + , streamStringView( userAgentHttpHeader, &txtOutStream ) + , (UInt64) serializedEvents.length, (int) serializedEvents.length, serializedEvents.begin ); + textOutputStreamRewind( &txtOutStream ); - if ( curl != NULL ) + if ( config->disableSend ) { - curl_easy_cleanup( curl ); - curl = NULL; + ELASTIC_APM_LOG_DEBUG( "disable_send (disableSend) configuration option is set to true - discarding events instead of sending" ); + ELASTIC_APM_CALL_EARLY_GOTO_FINALLY_WITH_SUCCESS(); } - if ( requestHeaders != NULL ) + if ( connectionData->curlHandle == NULL ) { - curl_slist_free_all( requestHeaders ); - requestHeaders = NULL; + ELASTIC_APM_CALL_IF_FAILED_GOTO( initConnectionData( config, connectionData, userAgentHttpHeader ) ); } + ELASTIC_APM_CALL_IF_FAILED_GOTO( syncSendEventsToApmServerWithConn( config, connectionData, serializedEvents ) ); + + resultCode = resultSuccess; + finally: ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); return resultCode; failure: + cleanupConnectionData( connectionData ); goto finally; } @@ -301,8 +359,6 @@ struct DataToSendNode DataToSendNode* prev; DataToSendNode* next; - bool disableSend; - double serverTimeoutMilliseconds; StringBuffer userAgentHttpHeader; StringBuffer serializedEvents; }; @@ -336,8 +392,6 @@ static void initDataToSendQueue( DataToSendQueue* dataQueue ) static ResultCode addCopyToDataToSendQueue( DataToSendQueue* dataQueue , UInt64 id - , bool disableSend - , double serverTimeoutMilliseconds , StringView userAgentHttpHeader , StringView serializedEvents ) { @@ -355,8 +409,6 @@ static ResultCode addCopyToDataToSendQueue( DataToSendQueue* dataQueue resultCode = resultSuccess; newNode->id = id; - newNode->disableSend = disableSend; - newNode->serverTimeoutMilliseconds = serverTimeoutMilliseconds; newNode->next = &( dataQueue->tail ); newNode->prev = dataQueue->tail.prev; @@ -424,7 +476,6 @@ struct BackgroundBackendComm DataToSendQueue dataToSendQueue; size_t dataToSendTotalSize; size_t nextEventsBatchId; - double lastServerTimeoutMilliseconds; bool shouldExit; TimeSpec shouldExitBy; }; @@ -623,8 +674,7 @@ ResultCode backgroundBackendCommThreadFunc_waitForChangesInSharedState( ResultCode backgroundBackendCommThreadFunc_sendFirstEventsBatch( const ConfigSnapshot* config - , const BackgroundBackendCommSharedStateSnapshot* sharedStateSnapshot -) + , const BackgroundBackendCommSharedStateSnapshot* sharedStateSnapshot ) { // This function is called only when data-queue-to-send is not empty // so firstDataToSendNode is not NULL @@ -641,10 +691,8 @@ ResultCode backgroundBackendCommThreadFunc_sendFirstEventsBatch( ResultCode resultCode; - resultCode = syncSendEventsToApmServer( sharedStateSnapshot->firstDataToSendNode->disableSend - , sharedStateSnapshot->firstDataToSendNode->serverTimeoutMilliseconds - , config - , sharedStateSnapshot->firstDataToSendNode->userAgentHttpHeader.begin + resultCode = syncSendEventsToApmServer( config + , viewStringBuffer( sharedStateSnapshot->firstDataToSendNode->userAgentHttpHeader ) , serializedEvents ); // If we failed to send the currently first batch we return success nevertheless // it means that this batch will be removed, and we will continue on to sending the rest of the queued events @@ -822,7 +870,6 @@ ResultCode newBackgroundBackendComm( const ConfigSnapshot* config, BackgroundBac initDataToSendQueue( &( backgroundBackendComm->dataToSendQueue ) ); backgroundBackendComm->dataToSendTotalSize = 0; backgroundBackendComm->nextEventsBatchId = 1; - backgroundBackendComm->lastServerTimeoutMilliseconds = 0; backgroundBackendComm->shouldExit = false; ELASTIC_APM_CALL_IF_FAILED_GOTO( newMutex( &( backgroundBackendComm->mutex ), /* dbgDesc */ "Background backend communications" ) ); ELASTIC_APM_CALL_IF_FAILED_GOTO( newConditionVariable( &( backgroundBackendComm->condVar ), /* dbgDesc */ "Background backend communications" ) ); @@ -870,38 +917,40 @@ ResultCode backgroundBackendCommEnsureInited( const ConfigSnapshot* config ) } static -ResultCode signalBackgroundBackendCommThreadToExit( BackgroundBackendComm* backgroundBackendComm, /* out */ TimeSpec* shouldExitBy ) +ResultCode signalBackgroundBackendCommThreadToExit( const ConfigSnapshot* config + , BackgroundBackendComm* backgroundBackendComm + , /* out */ TimeSpec* shouldExitBy ) { ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); ResultCode resultCode; bool shouldUnlockMutex = false; + char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE ]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); ELASTIC_APM_CALL_IF_FAILED_GOTO( lockMutex( backgroundBackendComm->mutex, &shouldUnlockMutex, __FUNCTION__ ) ); backgroundBackendComm->shouldExit = true; ELASTIC_APM_CALL_IF_FAILED_GOTO( getCurrentAbsTimeSpec( /* out */ shouldExitBy ) ); - addDelayToAbsTimeSpec( /* in, out */ shouldExitBy, lround( backgroundBackendComm->lastServerTimeoutMilliseconds * ELASTIC_APM_NUMBER_OF_NANOSECONDS_IN_MILLISECOND ) ); + addDelayToAbsTimeSpec( /* in, out */ shouldExitBy, (long)durationToMilliseconds( config->serverTimeout ) * ELASTIC_APM_NUMBER_OF_NANOSECONDS_IN_MILLISECOND ); backgroundBackendComm->shouldExitBy = *shouldExitBy; ELASTIC_APM_CALL_IF_FAILED_GOTO( signalConditionVariable( backgroundBackendComm->condVar, __FUNCTION__ ) ); resultCode = resultSuccess; - finally: unlockMutex( backgroundBackendComm->mutex, &shouldUnlockMutex, __FUNCTION__ ); - char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE ]; - TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG( - "shouldExitBy: %s, backgroundBackendComm->lastServerTimeoutMilliseconds: %f" - , streamUtcTimeSpecAsLocal( shouldExitBy, &txtOutStream ), backgroundBackendComm->lastServerTimeoutMilliseconds ); + "shouldExitBy: %s, serverTimeout: %s" + , streamUtcTimeSpecAsLocal( shouldExitBy, &txtOutStream ), streamDuration( config->serverTimeout, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); return resultCode; failure: goto finally; } -void backgroundBackendCommOnModuleShutdown() +void backgroundBackendCommOnModuleShutdown( const ConfigSnapshot* config ) { BackgroundBackendComm* backgroundBackendComm = g_backgroundBackendComm; @@ -913,12 +962,12 @@ void backgroundBackendCommOnModuleShutdown() ResultCode resultCode; TimeSpec shouldExitBy; - ELASTIC_APM_CALL_IF_FAILED_GOTO( signalBackgroundBackendCommThreadToExit( backgroundBackendComm, /* out */ &shouldExitBy ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( signalBackgroundBackendCommThreadToExit( config, backgroundBackendComm, /* out */ &shouldExitBy ) ); ELASTIC_APM_CALL_IF_FAILED_GOTO( unwindBackgroundBackendComm( &backgroundBackendComm, &shouldExitBy, /* isCreatedByThisProcess */ true ) ); - resultCode = resultSuccess; + resultCode = resultSuccess; finally: - + cleanupConnectionData( &g_connectionData ); g_backgroundBackendComm = NULL; return; @@ -927,23 +976,18 @@ void backgroundBackendCommOnModuleShutdown() } static -ResultCode enqueueEventsToSendToApmServer( - bool disableSend - , double serverTimeoutMilliseconds - , StringView userAgentHttpHeader - , StringView serializedEvents ) +ResultCode enqueueEventsToSendToApmServer( StringView userAgentHttpHeader, StringView serializedEvents ) { - long serverTimeoutMillisecondsLong = (long) ceil( serverTimeoutMilliseconds ); + char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE ]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + ELASTIC_APM_LOG_DEBUG( "Queueing events to send asynchronously..." - "; disableSend: %s" - "; serverTimeoutMilliseconds: %f (as integer: %"PRIu64")" "; userAgentHttpHeader [length: %"PRIu64"]: `%.*s'" "; serializedEvents [length: %"PRIu64"]:\n%.*s" - , boolToString( disableSend ) - , serverTimeoutMilliseconds, (UInt64) serverTimeoutMillisecondsLong , (UInt64) userAgentHttpHeader.length, (int) userAgentHttpHeader.length, userAgentHttpHeader.begin , (UInt64) serializedEvents.length, (int) serializedEvents.length, serializedEvents.begin ); + textOutputStreamRewind( &txtOutStream ); ResultCode resultCode; bool shouldUnlockMutex = false; @@ -964,14 +1008,11 @@ ResultCode enqueueEventsToSendToApmServer( ELASTIC_APM_CALL_IF_FAILED_GOTO( addCopyToDataToSendQueue( &( backgroundBackendComm->dataToSendQueue ) , id - , disableSend - , serverTimeoutMilliseconds , userAgentHttpHeader , serializedEvents ) ); backgroundBackendComm->dataToSendTotalSize += serializedEvents.length; ++backgroundBackendComm->nextEventsBatchId; - backgroundBackendComm->lastServerTimeoutMilliseconds = serverTimeoutMilliseconds; ELASTIC_APM_LOG_DEBUG( "Queued a batch of events" @@ -991,11 +1032,7 @@ ResultCode enqueueEventsToSendToApmServer( ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG( "Finished queueing events to send asynchronously" - "; disableSend: %s" - "; serverTimeoutMilliseconds: %f (as integer: %"PRIu64")" "; serializedEvents [length: %"PRIu64"]:\n%.*s" - , boolToString( disableSend ) - , serverTimeoutMilliseconds, (UInt64) serverTimeoutMillisecondsLong , (UInt64) serializedEvents.length, (int) serializedEvents.length, serializedEvents.begin ); return resultCode; @@ -1004,54 +1041,23 @@ ResultCode enqueueEventsToSendToApmServer( goto finally; } -ResultCode sendEventsToApmServerWithDataConvertedForSync( - bool disableSend - , double serverTimeoutMilliseconds - , const ConfigSnapshot* config - , StringView userAgentHttpHeader - , StringView serializedEvents ) +ResultCode sendEventsToApmServer( const ConfigSnapshot* config, StringView userAgentHttpHeader, StringView serializedEvents ) { ResultCode resultCode; - StringBuffer userAgentHttpHeaderWithTermNull = { 0 }; - - ELASTIC_APM_CALL_IF_FAILED_GOTO( dupMallocStringView( userAgentHttpHeader, /* out */ &userAgentHttpHeaderWithTermNull ) ); - - ELASTIC_APM_CALL_IF_FAILED_GOTO( - syncSendEventsToApmServer( disableSend - , serverTimeoutMilliseconds - , config - , userAgentHttpHeaderWithTermNull.begin - , serializedEvents ) ); - - resultCode = resultSuccess; - finally: - freeMallocedStringBuffer( /* in,out */ &userAgentHttpHeaderWithTermNull ); - return resultCode; - - failure: - goto finally; -} - -ResultCode sendEventsToApmServer( - bool disableSend - , double serverTimeoutMilliseconds - , const ConfigSnapshot* config - , StringView userAgentHttpHeader - , StringView serializedEvents ) -{ - ResultCode resultCode; - long serverTimeoutMillisecondsLong = (long) ceil( serverTimeoutMilliseconds ); + char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE ]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); ELASTIC_APM_LOG_DEBUG( "Handling request to send events..." - " disableSend: %s" - "; serverTimeoutMilliseconds: %f (as integer: %"PRIu64")" + "; config: { serverUrl: %s, disableSend: %s, serverTimeout: %s }" "; userAgentHttpHeader [length: %"PRIu64"]: `%.*s'" "; serializedEvents [length: %"PRIu64"]:\n%.*s" - , boolToString( disableSend ) - , serverTimeoutMilliseconds, (UInt64) serverTimeoutMillisecondsLong + , config->serverUrl + , boolToString( config->disableSend ) + , streamDuration( config->serverTimeout, &txtOutStream ) , (UInt64) userAgentHttpHeader.length, (int) userAgentHttpHeader.length, userAgentHttpHeader.begin , (UInt64) serializedEvents.length, (int) serializedEvents.length, serializedEvents.begin ); + textOutputStreamRewind( &txtOutStream ); String dbgAsyncBackendCommReason = NULL; bool shouldSendAsync = deriveAsyncBackendComm( config, &dbgAsyncBackendCommReason ); @@ -1060,16 +1066,11 @@ ResultCode sendEventsToApmServer( if ( shouldSendAsync ) { ELASTIC_APM_CALL_IF_FAILED_GOTO( backgroundBackendCommEnsureInited( config ) ); - ELASTIC_APM_CALL_IF_FAILED_GOTO( enqueueEventsToSendToApmServer( disableSend, serverTimeoutMilliseconds, userAgentHttpHeader, serializedEvents ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( enqueueEventsToSendToApmServer( userAgentHttpHeader, serializedEvents ) ); } else { - ELASTIC_APM_CALL_IF_FAILED_GOTO( sendEventsToApmServerWithDataConvertedForSync( - disableSend - , serverTimeoutMilliseconds - , config - , userAgentHttpHeader - , serializedEvents ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( syncSendEventsToApmServer( config, userAgentHttpHeader, serializedEvents ) ); } resultCode = resultSuccess; diff --git a/src/ext/backend_comm.h b/src/ext/backend_comm.h index 8f5432334..2d543b77d 100644 --- a/src/ext/backend_comm.h +++ b/src/ext/backend_comm.h @@ -24,12 +24,10 @@ #include "ResultCode.h" ResultCode sendEventsToApmServer( - bool disableSend - , double serverTimeoutMilliseconds - , const ConfigSnapshot* config + const ConfigSnapshot* config , StringView userAgentHttpHeader , StringView serializedEvents ); -void backgroundBackendCommOnModuleShutdown(); +void backgroundBackendCommOnModuleShutdown( const ConfigSnapshot* config ); ResultCode resetBackgroundBackendCommStateInForkedChild(); diff --git a/src/ext/basic_macros.h b/src/ext/basic_macros.h index 497d89ead..a1e3a8b5d 100644 --- a/src/ext/basic_macros.h +++ b/src/ext/basic_macros.h @@ -136,3 +136,5 @@ //// ELASTIC_APM_IF_VA_ARGS_EMPTY_ELSE //// //////////////////////////////////////////////////////////////////////////////// + +#define ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( enumElement ) [enumElement] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PP_STRINGIZE( enumElement ) ) diff --git a/src/ext/config.m4 b/src/ext/config.m4 index 2ba4f0e9b..0eecd664f 100644 --- a/src/ext/config.m4 +++ b/src/ext/config.m4 @@ -92,6 +92,7 @@ if test "$PHP_ELASTIC_APM" != "no"; then php_error.c \ platform.c \ platform_threads_linux.c \ + ResultCode.c \ supportability.c \ SystemMetrics.c \ TextOutputStream.c \ diff --git a/src/ext/config.w32 b/src/ext/config.w32 index 1f4b3b87a..5321f310c 100644 --- a/src/ext/config.w32 +++ b/src/ext/config.w32 @@ -26,6 +26,7 @@ if (PHP_ELASTIC_APM != 'no') { "php_error.c" + " " + "platform.c" + " " + "platform_threads_windows.c" + " " + + "ResultCode.c" + " " + "supportability.c" + " " + "SystemMetrics.c" + " " + "time_util.c" + " " + diff --git a/src/ext/elastic_apm.c b/src/ext/elastic_apm.c index a86ff052c..3c335eb20 100644 --- a/src/ext/elastic_apm.c +++ b/src/ext/elastic_apm.c @@ -456,16 +456,12 @@ PHP_FUNCTION( elastic_apm_intercept_calls_to_internal_function ) } /* }}} */ -ZEND_BEGIN_ARG_INFO_EX( elastic_apm_send_to_server_arginfo, /* _unused: */ 0, /* return_reference: */ 0, /* required_num_args: */ 4 ) - ZEND_ARG_TYPE_INFO( /* pass_by_ref: */ 0, disableSend, IS_LONG, /* allow_null: */ 0 ) - ZEND_ARG_TYPE_INFO( /* pass_by_ref: */ 0, serverTimeoutMilliseconds, IS_DOUBLE, /* allow_null: */ 0 ) - ZEND_ARG_TYPE_INFO( /* pass_by_ref: */ 0, userAgentHttpHeader, IS_STRING, /* allow_null: */ 0 ) - ZEND_ARG_TYPE_INFO( /* pass_by_ref: */ 0, serializedEvents, IS_STRING, /* allow_null: */ 0 ) +ZEND_BEGIN_ARG_INFO_EX( elastic_apm_send_to_server_arginfo, /* _unused: */ 0, /* return_reference: */ 0, /* required_num_args: */ 2 ) + ZEND_ARG_TYPE_INFO( /* pass_by_ref: */ 0, userAgentHttpHeader, IS_STRING, /* allow_null: */ 0 ) + ZEND_ARG_TYPE_INFO( /* pass_by_ref: */ 0, serializedEvents, IS_STRING, /* allow_null: */ 0 ) ZEND_END_ARG_INFO() /* {{{ elastic_apm_send_to_server( - * int $disableSend - * float $serverTimeoutMilliseconds, * string userAgentHttpHeader, * string $serializedEvents ): bool */ @@ -478,27 +474,21 @@ PHP_FUNCTION( elastic_apm_send_to_server ) // which might deadlock in forked child ELASTIC_APM_CALL_IF_FAILED_GOTO( elasticApmApiEntered( __FILE__, __LINE__, __FUNCTION__ ) ); - long disableSend = 0; - double serverTimeoutMilliseconds = 0.0; char* userAgentHttpHeader = NULL; size_t userAgentHttpHeaderLength = 0; char* serializedEvents = NULL; size_t serializedEventsLength = 0; - ZEND_PARSE_PARAMETERS_START( /* min_num_args: */ 4, /* max_num_args: */ 4 ) - Z_PARAM_LONG( disableSend ) - Z_PARAM_DOUBLE( serverTimeoutMilliseconds ) - Z_PARAM_STRING( userAgentHttpHeader, userAgentHttpHeaderLength ) - Z_PARAM_STRING( serializedEvents, serializedEventsLength ) + ZEND_PARSE_PARAMETERS_START( /* min_num_args: */ 2, /* max_num_args: */ 2 ) + Z_PARAM_STRING( userAgentHttpHeader, userAgentHttpHeaderLength ) + Z_PARAM_STRING( serializedEvents, serializedEventsLength ) ZEND_PARSE_PARAMETERS_END(); ELASTIC_APM_CALL_IF_FAILED_GOTO( elasticApmSendToServer( - disableSend - , serverTimeoutMilliseconds - , makeStringView( userAgentHttpHeader, userAgentHttpHeaderLength ) + makeStringView( userAgentHttpHeader, userAgentHttpHeaderLength ) , makeStringView( serializedEvents, serializedEventsLength ) ) ); - retVal = true; + retVal = true; finally: RETURN_BOOL( retVal ); diff --git a/src/ext/elastic_apm_API.c b/src/ext/elastic_apm_API.c index bc02b48dc..80c0a3bd7 100644 --- a/src/ext/elastic_apm_API.c +++ b/src/ext/elastic_apm_API.c @@ -248,26 +248,16 @@ static inline bool longToBool( long longVal ) return longVal != 0; } -ResultCode elasticApmSendToServer( - long disableSend - , double serverTimeoutMilliseconds - , StringView userAgentHttpHeader - , StringView serializedEvents ) +ResultCode elasticApmSendToServer( StringView userAgentHttpHeader, StringView serializedEvents ) { ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); ResultCode resultCode; Tracer* const tracer = getGlobalTracer(); - ELASTIC_APM_CALL_IF_FAILED_GOTO( - sendEventsToApmServer( longToBool( disableSend ) - , serverTimeoutMilliseconds - , getTracerCurrentConfigSnapshot( tracer ) - , userAgentHttpHeader - , serializedEvents ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( sendEventsToApmServer( getTracerCurrentConfigSnapshot( tracer ), userAgentHttpHeader, serializedEvents ) ); resultCode = resultSuccess; - finally: ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); return resultCode; diff --git a/src/ext/elastic_apm_API.h b/src/ext/elastic_apm_API.h index 30991bbd4..221a9d4c2 100644 --- a/src/ext/elastic_apm_API.h +++ b/src/ext/elastic_apm_API.h @@ -39,10 +39,6 @@ ResultCode elasticApmInterceptCallsToInternalFunction( String functionName, uint void resetCallInterceptionOnRequestShutdown(); -ResultCode elasticApmSendToServer( - long disableSend - , double serverTimeoutMilliseconds - , StringView userAgentHttpHeader - , StringView serializedEvents ); +ResultCode elasticApmSendToServer( StringView userAgentHttpHeader, StringView serializedEvents ); ResultCode replaceSleepWithResumingAfterSignalImpl(); diff --git a/src/ext/lifecycle.c b/src/ext/lifecycle.c index cb071ddaf..2bc96f108 100644 --- a/src/ext/lifecycle.c +++ b/src/ext/lifecycle.c @@ -203,7 +203,7 @@ void elasticApmModuleShutdown( int moduleType, int moduleNumber ) goto finally; } - backgroundBackendCommOnModuleShutdown(); + backgroundBackendCommOnModuleShutdown( config ); if ( tracer->curlInited ) { diff --git a/src/ext/time_util.c b/src/ext/time_util.c index 5777f3a1f..824a93461 100644 --- a/src/ext/time_util.c +++ b/src/ext/time_util.c @@ -23,45 +23,55 @@ #include #include "log.h" #include "platform.h" +#include "util.h" +#include "basic_macros.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_UTIL -Duration makeDuration( Int64 value, DurationUnits units ) +StringView durationUnitsNames[ numberOfDurationUnits ] = { - switch (units) - { - case durationUnits_milliseconds: - return (Duration){ .valueInMilliseconds = value }; - - case durationUnits_seconds: - return (Duration){ .valueInMilliseconds = value * 1000 * 1000 }; + [ durationUnits_millisecond ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "ms" ), + [ durationUnits_second ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "s" ), + [ durationUnits_minute ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "m" ), +}; - case durationUnits_minutes: - return (Duration){ .valueInMilliseconds = value * 1000 * 1000 * 60 }; +ResultCode parseDuration( StringView inputString, DurationUnits defaultUnits, /* out */ Duration* result ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( result ); - default: - ELASTIC_APM_ASSERT( false, "Unknown duration units (as int): %d", units ); - return (Duration){ .valueInMilliseconds = value }; + size_t unitsIndex; + ResultCode resultCode = parseDecimalIntegerWithUnits( inputString, durationUnitsNames, numberOfDurationUnits, &result->valueInUnits, &unitsIndex ); + if ( resultCode == resultSuccess ) + { + result->units = ( unitsIndex == numberOfDurationUnits ? defaultUnits : (DurationUnits)unitsIndex ); } + return resultCode; } -ResultCode parseDuration( StringView valueAsString, DurationUnits defaultUnits, /* out */ Duration* result ) +String streamDuration( Duration duration, TextOutputStream* txtOutStream ) { - result->valueInMilliseconds = 10; - - return resultSuccess; + return isValidDurationUnits( duration.units ) + ? streamPrintf( txtOutStream, "%"PRId64"%s", duration.valueInUnits, durationUnitsToString( duration.units ) ) + : streamPrintf( txtOutStream, "%"PRId64"", duration.valueInUnits, duration.units ); } -String streamDuration( Duration duration, TextOutputStream* txtOutStream ) +Int64 durationToMilliseconds( Duration duration ) { - // so 5s and not 5000ms + switch (duration.units) + { + case durationUnits_millisecond: + return duration.valueInUnits; - return streamPrintf( txtOutStream, "%"PRIu64"ms", duration.valueInMilliseconds ); -} + case durationUnits_second: + return duration.valueInUnits * 1000; -double durationToMilliseconds( Duration duration ) -{ - return (double)duration.valueInMilliseconds; + case durationUnits_minute: + return duration.valueInUnits * 60 * 1000; + + default: + ELASTIC_APM_ASSERT( false, "Unknown duration units (as int): %d", duration.units ); + return duration.valueInUnits; + } } #ifdef PHP_WIN32 diff --git a/src/ext/time_util.h b/src/ext/time_util.h index 81394a09c..b2c2adec9 100644 --- a/src/ext/time_util.h +++ b/src/ext/time_util.h @@ -72,25 +72,51 @@ double durationMicrosecondsToMilliseconds( Int64 durationMicros ) enum DurationUnits { - durationUnits_milliseconds, - durationUnits_seconds, - durationUnits_minutes + durationUnits_millisecond, + durationUnits_second, + durationUnits_minute, + + numberOfDurationUnits }; typedef enum DurationUnits DurationUnits; +extern StringView durationUnitsNames[ numberOfDurationUnits ]; struct Duration { - Int64 valueInMilliseconds; + Int64 valueInUnits; + DurationUnits units; }; typedef struct Duration Duration; -Duration makeDuration( Int64 value, DurationUnits units ); +static inline +bool isValidDurationUnits( DurationUnits units ) +{ + return ( durationUnits_millisecond <= units ) && ( units < numberOfDurationUnits ); +} + +#define ELASTIC_APM_UNKNOWN_DURATION_UNITS_AS_STRING "" + +static inline +String durationUnitsToString( DurationUnits durationUnits ) +{ + if ( isValidDurationUnits( durationUnits ) ) + { + return durationUnitsNames[ durationUnits ].begin; + } + return ELASTIC_APM_UNKNOWN_DURATION_UNITS_AS_STRING; +} + +static inline +Duration makeDuration( Int64 valueInUnits, DurationUnits units ) +{ + return (Duration){ .valueInUnits = valueInUnits, .units = units }; +} -ResultCode parseDuration( StringView valueAsString, DurationUnits defaultUnits, /* out */ Duration* result ); +ResultCode parseDuration( StringView inputString, DurationUnits defaultUnits, /* out */ Duration* result ); String streamDuration( Duration duration, TextOutputStream* txtOutStream ); -double durationToMilliseconds( Duration duration ); +Int64 durationToMilliseconds( Duration duration ); ResultCode getCurrentAbsTimeSpec( /* out */ TimeSpec* currentAbsTimeSpec ); diff --git a/src/ext/unit_tests/CMakeLists.txt b/src/ext/unit_tests/CMakeLists.txt index e9bc588a7..071f5a605 100644 --- a/src/ext/unit_tests/CMakeLists.txt +++ b/src/ext/unit_tests/CMakeLists.txt @@ -22,6 +22,8 @@ PROJECT( unit_tests C ) SET( CMAKE_C_STANDARD 99 ) +SET( CMAKE_COMPILE_WARNING_AS_ERROR ON ) + # Always include srcdir and builddir in include path # This saves typing ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY} in # about every subdir @@ -77,21 +79,15 @@ IF ( WIN32 ) ADD_COMPILE_DEFINITIONS( _CRT_SECURE_NO_WARNINGS ) ENDIF() -#FIND_PACKAGE( cmocka REQUIRED ) -FIND_LIBRARY( cmocka_static_lib cmocka ) -IF ( NOT cmocka_static_lib ) - IF ( WIN32 ) - IF ( DEFINED ENV{cmocka_installed_dir} ) - SET( cmocka_installed_dir $ENV{cmocka_installed_dir} ) - ELSE() - IF ( NOT $ENV{CLION_IDE} ) - MESSAGE( FATAL_ERROR "cmocka_installed_dir environment variable is not defined" ) - ENDIF() - ENDIF() - SET( cmocka_include_dir ${cmocka_installed_dir}/include ) - SET( cmocka_lib_dir ${cmocka_installed_dir}/lib/x64_Debug ) - SET( cmocka_static_lib ${cmocka_lib_dir}/cmocka-static.lib ) - ELSE() +IF ( DEFINED ENV{cmocka_installed_dir} ) + SET( cmocka_installed_dir $ENV{cmocka_installed_dir} ) + SET( cmocka_include_dir ${cmocka_installed_dir}/include ) + SET( cmocka_lib_dir ${cmocka_installed_dir}/lib ) + SET( cmocka_static_lib ${cmocka_lib_dir}/cmocka.lib ) +ELSE() + #FIND_PACKAGE( cmocka REQUIRED ) + FIND_LIBRARY( cmocka_static_lib cmocka ) + IF ( NOT cmocka_static_lib ) MESSAGE( FATAL_ERROR "cmocka library not found" ) ENDIF() ENDIF() @@ -112,6 +108,7 @@ LIST( APPEND source_files ${src_ext_dir}/log.h ${src_ext_dir}/log.c ) LIST( APPEND source_files ${src_ext_dir}/MemoryTracker.h ${src_ext_dir}/MemoryTracker.c ) LIST( APPEND source_files ${src_ext_dir}/platform.h ${src_ext_dir}/platform.c ) LIST( APPEND source_files ${src_ext_dir}/platform_threads.h ${src_ext_dir}/platform_threads_linux.c ) +LIST( APPEND source_files ${src_ext_dir}/ResultCode.h ${src_ext_dir}/ResultCode.c ) LIST( APPEND source_files ${src_ext_dir}/TextOutputStream.h ${src_ext_dir}/TextOutputStream.c ) LIST( APPEND source_files ${src_ext_dir}/time_util.h ${src_ext_dir}/time_util.c ) LIST( APPEND source_files ${src_ext_dir}/Tracer.h ${src_ext_dir}/Tracer.c ) diff --git a/src/ext/unit_tests/ResultCode_tests.c b/src/ext/unit_tests/ResultCode_tests.c new file mode 100644 index 000000000..14cf6c271 --- /dev/null +++ b/src/ext/unit_tests/ResultCode_tests.c @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "unit_test_util.h" + +#include +#include "ResultCode.h" + +static +void test_isValidResultCode( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + ELASTIC_APM_FOR_EACH_INDEX( resultCode, numberOfResultCodes ) + { + ELASTIC_APM_CMOCKA_ASSERT_MSG( isValidResultCode( resultCode ), "resultCode: %d", (int)resultCode ); + } +} + +static +void test_resultCodeToString( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + ELASTIC_APM_FOR_EACH_INDEX( resultCode, numberOfResultCodes ) + { + ELASTIC_APM_CMOCKA_ASSERT_STRING_EQUAL( resultCodeNames[ resultCode ].begin, resultCodeToString( resultCode ), "resultCode: %d", (int)resultCode ); + } + + int valuesUnknownResultCodes[] = { -1, INT_MIN, numberOfResultCodes, numberOfResultCodes + 1, numberOfResultCodes + 10, 2 * numberOfResultCodes }; + ELASTIC_APM_FOR_EACH_INDEX( i, ELASTIC_APM_STATIC_ARRAY_SIZE( valuesUnknownResultCodes ) ) + { + int value = valuesUnknownResultCodes[ i ]; + String valueAsResultCodeString = resultCodeToString( value ); + ELASTIC_APM_CMOCKA_ASSERT_STRING_EQUAL( ELASTIC_APM_UNKNOWN_RESULT_CODE_AS_STRING, valueAsResultCodeString, "i: %d, value: %d", (int)i, value ); + } +} + +int run_ResultCode_tests( int argc, const char* argv[] ) +{ + const struct CMUnitTest tests [] = + { + ELASTIC_APM_CMOCKA_UNIT_TEST( test_isValidResultCode ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_resultCodeToString ), + }; + + return cmocka_run_group_tests( tests, NULL, NULL ); +} diff --git a/src/ext/unit_tests/main.c b/src/ext/unit_tests/main.c index b248085dc..e357287d2 100644 --- a/src/ext/unit_tests/main.c +++ b/src/ext/unit_tests/main.c @@ -49,6 +49,8 @@ int run_Logger_tests(); int run_config_tests(); int run_time_util_tests(); int run_iterateOverCStackTrace_tests(); +int run_ResultCode_tests(); +int run_parse_value_with_units_tests(); int main( int argc, char* argv[] ) { @@ -71,6 +73,8 @@ int main( int argc, char* argv[] ) failedTestsCount += run_config_tests(); failedTestsCount += run_time_util_tests(); failedTestsCount += run_iterateOverCStackTrace_tests(); + failedTestsCount += run_ResultCode_tests(); + failedTestsCount += run_parse_value_with_units_tests(); // gen_numbered_intercepting_callbacks_src( 1000 ); diff --git a/src/ext/unit_tests/parse_value_with_units_tests.c b/src/ext/unit_tests/parse_value_with_units_tests.c new file mode 100644 index 000000000..da91a94fd --- /dev/null +++ b/src/ext/unit_tests/parse_value_with_units_tests.c @@ -0,0 +1,493 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "unit_test_util.h" +#include +#include "time_util.h" +#include "util.h" + +typedef bool ( * IsValidUnitsGenericWrapper )( int value ); +typedef String ( * UnitsToStringGenericWrapper )( int value ); +typedef Int64 ( * GetValueInUnitsGenericWrapper )( const void* valueWithUnits ); +typedef int ( * GetUnitsGenericWrapper )( const void* valueWithUnits ); +typedef ResultCode ( * ParseValueWithUnitsGenericWrapper )( StringView inputString, int defaultUnits, /* out */ void* result ); +typedef void ( * AssignValueWithUnitsGenericWrapper )( const void* src, /* out */ void* dst ); +typedef void* ( * AllocValueWithUnitsGenericWrapper )(); +typedef void ( * FillValueWithUnitsGenericWrapper )( Int64 valueInUnits, int units, void* dst ); + +struct UnitsTypeMetaData +{ + String dbgUnitsTypeAsString; + int numberOfUnits; + StringView* unitsNames; + IsValidUnitsGenericWrapper isValidUnits; + String invalidUnitsAsString; + IntArrayView invalidUnits; + UnitsToStringGenericWrapper unitsToString; + GetValueInUnitsGenericWrapper getValueInUnits; + GetUnitsGenericWrapper getUnits; + ParseValueWithUnitsGenericWrapper parseValueWithUnits; + const void* invalidValueWithUnits; + AssignValueWithUnitsGenericWrapper assignValueWithUnits; + AllocValueWithUnitsGenericWrapper allocValueWithUnits; + FillValueWithUnitsGenericWrapper fillValueWithUnits; +}; +typedef struct UnitsTypeMetaData UnitsTypeMetaData; + +#define ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( prefix, unitsPart, suffix ) ELASTIC_APM_PP_CONCAT( prefix, ELASTIC_APM_PP_CONCAT( unitsPart, suffix ) ) + +#define ELASTIC_APM_UNITS_STRUCT_NAME( UnitsType ) ELASTIC_APM_PP_CONCAT( UnitsType, Units ) + +#define ELASTIC_APM_IS_VALID_UNITS( UnitsType ) ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( isValid, UnitsType, Units ) + +#define ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( numberOf, UnitsType, Units ) + +#define ELASTIC_APM_UNITS_NAMES( unitsPrefix ) ELASTIC_APM_PP_CONCAT( unitsPrefix, UnitsNames ) + +#define ELASTIC_APM_IS_VALID_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( isValid, UnitsType, UnitsGenericWrapper ) + +#define ELASTIC_APM_DEFINE_IS_VALID_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + bool ELASTIC_APM_IS_VALID_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )( int value ) \ + { \ + return ELASTIC_APM_IS_VALID_UNITS( UnitsType )( (ELASTIC_APM_UNITS_STRUCT_NAME( UnitsType ))value ); \ + } + +#define ELASTIC_APM_INVALID_UNITS_ARRAY_NAME( unitsPrefix ) ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( g_, unitsPrefix, InvalidUnits ) + +#define ELASTIC_APM_DEFINE_INVALID_UNITS( UnitsType, unitsPrefix ) \ + static int ELASTIC_APM_INVALID_UNITS_ARRAY_NAME( unitsPrefix )[] = { \ + INT_MIN \ + , -ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) \ + , -1 \ + , ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) \ + , ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) + 1 \ + , ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) + 10 \ + , 2 * ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) \ + } + +#define ELASTIC_APM_UNITS_TO_STRING_GENERIC_WRAPPER_FUNC_NAME( unitsPrefix ) \ + ELASTIC_APM_PP_CONCAT( unitsPrefix, UnitsToStringGenericWrapper ) + +#define ELASTIC_APM_DEFINE_UNITS_TO_STRING_GENERIC_WRAPPER_FUNC( UnitsType, unitsPrefix ) \ + static \ + String ELASTIC_APM_UNITS_TO_STRING_GENERIC_WRAPPER_FUNC_NAME( unitsPrefix )( int value ) \ + { \ + return ELASTIC_APM_PP_CONCAT( unitsPrefix, UnitsToString )( (ELASTIC_APM_UNITS_STRUCT_NAME( UnitsType ))value ); \ + } + +#define ELASTIC_APM_GET_VALUE_IN_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( get, UnitsType, ValueInUnitsGenericWrapper ) + +#define ELASTIC_APM_DEFINE_GET_VALUE_IN_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + Int64 ELASTIC_APM_GET_VALUE_IN_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )( const void* valueWithUnits ) \ + { \ + return ((const UnitsType*)valueWithUnits)->valueInUnits; \ + } + +#define ELASTIC_APM_GET_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( get, UnitsType, UnitsGenericWrapper ) + +#define ELASTIC_APM_DEFINE_GET_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + int ELASTIC_APM_GET_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )( const void* valueWithUnits ) \ + { \ + return ((const UnitsType*)valueWithUnits)->units; \ + } + +#define ELASTIC_APM_PARSE_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( parse, UnitsType, GenericWrapper ) + +#define ELASTIC_APM_DEFINE_PARSE_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + ResultCode ELASTIC_APM_PARSE_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )( StringView inputString, int defaultUnits, /* out */ void* result ) \ + { \ + return ELASTIC_APM_PP_CONCAT( parse, UnitsType )( inputString, (ELASTIC_APM_UNITS_STRUCT_NAME( UnitsType ))defaultUnits, (UnitsType*)result ); \ + } + +#define ELASTIC_APM_ASSIGN_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( assign, UnitsType, GenericWrapper ) + +#define ELASTIC_APM_DEFINE_ASSIGN_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + void ELASTIC_APM_ASSIGN_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )( const void* src, /* out */ void* dst ) \ + { \ + *((UnitsType*)dst) = *((const UnitsType*)src); \ + } + +#define ELASTIC_APM_ALLOC_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( alloc, UnitsType, GenericWrapper ) + +#define ELASTIC_APM_DEFINE_ALLOC_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + void* ELASTIC_APM_ALLOC_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )() \ + { \ + return malloc( sizeof( UnitsType ) ); \ + } + +#define ELASTIC_APM_FILL_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( fill, UnitsType, GenericWrapper ) + +#define ELASTIC_APM_DEFINE_FILL_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static \ + void ELASTIC_APM_FILL_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType )( Int64 valueInUnits, int units, void* dst ) \ + { \ + ((UnitsType*)dst)->valueInUnits = valueInUnits; \ + ((UnitsType*)dst)->units = units; \ + } + +#define ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( unitsPrefix ) ELASTIC_APM_BUILD_NAME_WITH_UNITS_TYPE( g_, unitsPrefix, MetaData ) + +#define ELASTIC_APM_DEFINE_VALUE_WITH_UNITS_TYPE_META_DATA( UnitsType, unitsPrefix, invalidUnitsAsStringParam, pInvalidValueWithUnitsParam ) \ + ELASTIC_APM_DEFINE_IS_VALID_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + ELASTIC_APM_DEFINE_INVALID_UNITS( UnitsType, unitsPrefix ); \ + ELASTIC_APM_DEFINE_UNITS_TO_STRING_GENERIC_WRAPPER_FUNC( UnitsType, unitsPrefix ) \ + ELASTIC_APM_DEFINE_GET_VALUE_IN_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + ELASTIC_APM_DEFINE_GET_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + ELASTIC_APM_DEFINE_PARSE_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + ELASTIC_APM_DEFINE_ASSIGN_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + ELASTIC_APM_DEFINE_ALLOC_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + ELASTIC_APM_DEFINE_FILL_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC( UnitsType ) \ + static UnitsTypeMetaData ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( unitsPrefix ) = { \ + .dbgUnitsTypeAsString = ELASTIC_APM_PP_STRINGIZE( UnitsType ) \ + , .numberOfUnits = ELASTIC_APM_NUMBER_OF_UNITS( UnitsType ) \ + , .unitsNames = ELASTIC_APM_UNITS_NAMES( unitsPrefix ) \ + , .isValidUnits = &( ELASTIC_APM_IS_VALID_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) ) \ + , .invalidUnitsAsString = (invalidUnitsAsStringParam) \ + , .invalidUnits = ELASTIC_APM_STATIC_ARRAY_TO_VIEW( IntArrayView, ELASTIC_APM_INVALID_UNITS_ARRAY_NAME( unitsPrefix ) ) \ + , .unitsToString = &( ELASTIC_APM_UNITS_TO_STRING_GENERIC_WRAPPER_FUNC_NAME( unitsPrefix ) ) \ + , .getValueInUnits = &( ELASTIC_APM_GET_VALUE_IN_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) ) \ + , .getUnits = &( ELASTIC_APM_GET_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) ) \ + , .parseValueWithUnits = &( ELASTIC_APM_PARSE_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) ) \ + , .invalidValueWithUnits = (pInvalidValueWithUnitsParam) \ + , .assignValueWithUnits = ELASTIC_APM_ASSIGN_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + , .allocValueWithUnits = ELASTIC_APM_ALLOC_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + , .fillValueWithUnits = ELASTIC_APM_FILL_VALUE_WITH_UNITS_GENERIC_WRAPPER_FUNC_NAME( UnitsType ) \ + } + +static +void impl_test_isValidUnits( UnitsTypeMetaData unitsTypeMetaData ) +{ + ELASTIC_APM_FOR_EACH_INDEX( validValue, unitsTypeMetaData.numberOfUnits ) + { + ELASTIC_APM_CMOCKA_ASSERT_MSG( unitsTypeMetaData.isValidUnits( validValue ), "%s units validValue: %d", unitsTypeMetaData.dbgUnitsTypeAsString, (int)validValue ); + } + + int invalidValues[] = { -1, -unitsTypeMetaData.numberOfUnits, unitsTypeMetaData.numberOfUnits, unitsTypeMetaData.numberOfUnits + 1 }; + ELASTIC_APM_FOR_EACH_INDEX( invalidValueIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( invalidValues ) ) + { + int invalidValue = invalidValues[ invalidValueIndex ]; + ELASTIC_APM_CMOCKA_ASSERT_MSG( ! unitsTypeMetaData.isValidUnits( invalidValue ), "%s units: %d", unitsTypeMetaData.dbgUnitsTypeAsString, invalidValue ); + } +} + +static +void impl_test_unitsToString( UnitsTypeMetaData unitsTypeMetaData ) +{ + int numberOfUnits = unitsTypeMetaData.numberOfUnits; + ELASTIC_APM_FOR_EACH_INDEX_EX( int, validUnits, numberOfUnits ) + { + ELASTIC_APM_CMOCKA_ASSERT_STRING_EQUAL( unitsTypeMetaData.unitsNames[ validUnits ].begin, unitsTypeMetaData.unitsToString( (int) validUnits ) + , "%s units validValue: %d", unitsTypeMetaData.dbgUnitsTypeAsString, validUnits ); + } + + ELASTIC_APM_FOR_EACH_INDEX( invalidValueIndex, unitsTypeMetaData.invalidUnits.size ) + { + int invalidValue = unitsTypeMetaData.invalidUnits.values[ invalidValueIndex ]; + ELASTIC_APM_CMOCKA_ASSERT_STRING_EQUAL( unitsTypeMetaData.invalidUnitsAsString, unitsTypeMetaData.unitsToString( invalidValue ) + , "%s units invalidValue: %d", unitsTypeMetaData.dbgUnitsTypeAsString, invalidValue ); + } +} + +static +size_t numberOfValidUnits( StringView unitsBase ) +{ + if ( unitsBase.length == 0 ) + { + return 1; + } + + if ( unitsBase.length == 1 ) + { + return 2; + } + + return /* upper at each position */ unitsBase.length + /* all lower + upper */ 2; +} + +static +void generateValidUnits( StringView unitsBase, size_t variantIndex, char* buffer ) +{ + ELASTIC_APM_FOR_EACH_INDEX( i, unitsBase.length ) + { + buffer[ i ] = charToLowerCase( unitsBase.begin[ i ] ); + } + buffer[ unitsBase.length ] = '\0'; + + if ( unitsBase.length == 0 ) + { + return ; + } + + if ( unitsBase.length == 1 ) + { + if ( variantIndex > 0 ) + { + buffer[ 0 ] = charToUpperCase( buffer[ 0 ] ); + } + return; + } + + if ( variantIndex < unitsBase.length ) + { + buffer[ variantIndex ] = charToUpperCase( buffer[ variantIndex ] ); + return; + } + + ELASTIC_APM_FOR_EACH_INDEX( i, unitsBase.length ) + { + buffer[ i ] = charToUpperCase( unitsBase.begin[ i ] ); + } +} + +static +void assertEqualValuesWithUnitsGeneric( UnitsTypeMetaData unitsTypeMetaData, StringView inputString, const void* expected, const void* actual ) +{ + ELASTIC_APM_CMOCKA_ASSERT_MSG( unitsTypeMetaData.getUnits( expected ) == unitsTypeMetaData.getUnits( actual ) + , "units: %s; inputString: %s; units: expected: %s (as int: %d), actual: %s (as int: %d)" + , unitsTypeMetaData.dbgUnitsTypeAsString, inputString.begin + , unitsTypeMetaData.unitsToString( unitsTypeMetaData.getUnits( expected ) ), unitsTypeMetaData.getUnits( expected ) + , unitsTypeMetaData.unitsToString( unitsTypeMetaData.getUnits( actual ) ), unitsTypeMetaData.getUnits( actual ) ); + + ELASTIC_APM_CMOCKA_ASSERT_MSG( unitsTypeMetaData.getValueInUnits( expected ) == unitsTypeMetaData.getValueInUnits( actual ) + , "units: %s; inputString: %s; valueInUnits: expected: %"PRId64", actual: %"PRId64 + , unitsTypeMetaData.dbgUnitsTypeAsString, inputString.begin + , unitsTypeMetaData.getValueInUnits( expected ), unitsTypeMetaData.getValueInUnits( actual ) ); +} + +static +void impl_test_one_parse( UnitsTypeMetaData unitsTypeMetaData, StringView inputString, int defaultUnits, const void* expected ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + void* actual = unitsTypeMetaData.allocValueWithUnits(); + unitsTypeMetaData.assignValueWithUnits( unitsTypeMetaData.invalidValueWithUnits, /* dst */ actual ); + ResultCode expectedResultCode = expected == NULL ? resultParsingFailed : resultSuccess; + ResultCode actualResultCode = unitsTypeMetaData.parseValueWithUnits( inputString, defaultUnits, /* out */ actual ); + ELASTIC_APM_CMOCKA_ASSERT_MSG( + actualResultCode == expectedResultCode + , "units: %s; expectedResultCode: %s (%d), actualResultCode: %s (%d), inputString: `%s', defaultUnits: %s" + , unitsTypeMetaData.dbgUnitsTypeAsString + , resultCodeToString( expectedResultCode ), (int)expectedResultCode + , resultCodeToString( actualResultCode ), (int)actualResultCode + , streamStringView( inputString, &txtOutStream ), unitsTypeMetaData.unitsToString( defaultUnits ) ); + assertEqualValuesWithUnitsGeneric( unitsTypeMetaData, inputString, expected == NULL ? unitsTypeMetaData.invalidValueWithUnits : expected, actual ); + free( actual ); + actual = NULL; +} + +static +void impl_test_one_valid_parse( UnitsTypeMetaData unitsTypeMetaData, String whiteSpace, String units, const void* expected ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + String inputString = streamPrintf( &txtOutStream, "%s%"PRId64"%s%s%s", whiteSpace, unitsTypeMetaData.getValueInUnits( expected ), whiteSpace, units, whiteSpace ); + int expectedUnits = unitsTypeMetaData.getUnits( expected ); + int differentUnits = ( expectedUnits + 1 ) % unitsTypeMetaData.numberOfUnits; + ELASTIC_APM_CMOCKA_ASSERT( unitsTypeMetaData.isValidUnits( differentUnits ) ); + ELASTIC_APM_CMOCKA_ASSERT( differentUnits != expectedUnits ); + int defaultUnits = isEmtpyString( units ) ? expectedUnits : differentUnits; + impl_test_one_parse( unitsTypeMetaData, stringToView( inputString ), defaultUnits, expected ); +} + +static +void impl_test_one_invalid_parse( UnitsTypeMetaData unitsTypeMetaData, String whiteSpace, String invalidValue, int defaultUnits ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + String inputString = streamPrintf( &txtOutStream, "%s%s%s", whiteSpace, invalidValue, whiteSpace ); + impl_test_one_parse( unitsTypeMetaData, stringToView( inputString ), defaultUnits, /* expected */ NULL ); +} + +static +void impl_test_parse( UnitsTypeMetaData unitsTypeMetaData ) +{ + /////////////////////////////////////////////////////////////////// + // + // Valid + // + + Int64 validValueInUnitsVariants[] = { + 0, 1, 234, INT8_MAX, INT16_MAX, 567890, INT_MAX, INT32_MAX + , -1, -234, -INT8_MAX, INT8_MIN, -567890, -INT_MAX, INT_MIN, -INT32_MAX, INT32_MIN + }; + String validWhiteSpaceVariants[] = { "", " ", " ", "\t", " \t " }; + bool includeUnitsVariants[] = { true, false }; + enum { generatedUnitsBufferSize = 2 +1 /* +1 for terminating '\0' */ }; + char generatedUnitsBuffer[ generatedUnitsBufferSize ]; + void* expected = unitsTypeMetaData.allocValueWithUnits(); + + ELASTIC_APM_FOR_EACH_INDEX( validValueInUnitsVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( validValueInUnitsVariants ) ) + { + Int64 valueInUnits = validValueInUnitsVariants[ validValueInUnitsVariantIndex ]; + ELASTIC_APM_FOR_EACH_INDEX( whiteSpaceVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( validWhiteSpaceVariants ) ) + { + String whiteSpace = validWhiteSpaceVariants[ whiteSpaceVariantIndex ]; + ELASTIC_APM_FOR_EACH_INDEX_EX( int, units, unitsTypeMetaData.numberOfUnits ) + { + unitsTypeMetaData.fillValueWithUnits( valueInUnits, units, /* dst */ expected ); + ELASTIC_APM_FOR_EACH_INDEX( includeUnitsVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( includeUnitsVariants ) ) + { + bool includeUnits = includeUnitsVariants[ includeUnitsVariantIndex ]; + StringView unitsBase = includeUnits ? unitsTypeMetaData.unitsNames[ units ] : makeEmptyStringView(); + ELASTIC_APM_CMOCKA_ASSERT_MSG( generatedUnitsBufferSize >= ( unitsBase.length + 1 ) + , "generatedUnitsBufferSize: %d, unitsBase [length: %d]: %.*s" + , generatedUnitsBufferSize, ((int)unitsBase.length), ((int)unitsBase.length), unitsBase.begin ); + ELASTIC_APM_FOR_EACH_INDEX( unitsVariantIndex, numberOfValidUnits( unitsBase ) ) + { + generateValidUnits( unitsBase, unitsVariantIndex, generatedUnitsBuffer ); + ELASTIC_APM_CMOCKA_ASSERT( includeUnits == ( ! isEmtpyString( generatedUnitsBuffer ) ) ); + impl_test_one_valid_parse( unitsTypeMetaData, whiteSpace, generatedUnitsBuffer, expected ); + } + } + } + } + } + free( expected ); + expected = NULL; + + ELASTIC_APM_FOR_EACH_INDEX( whiteSpaceVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( validWhiteSpaceVariants ) ) + { + String whiteSpace = validWhiteSpaceVariants[ whiteSpaceVariantIndex ]; + ELASTIC_APM_FOR_EACH_INDEX_EX( int, defaultUnitsAsInt, unitsTypeMetaData.numberOfUnits ) + { + DurationUnits defaultUnits = (DurationUnits)defaultUnitsAsInt; + + /////////////////////////////////////////////////////////////////// + // + // Empty value in units + // + + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "", defaultUnits ); + + /////////////////////////////////////////////////////////////////// + // + // Invalid numeric part + // + + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "0x1", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "x0", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "0x", defaultUnits ); + + /////////////////////////////////////////////////////////////////// + // + // Invalid separator + // + + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1-", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1:", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1.", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1 - ", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1 : ", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1 . ", defaultUnits ); + + /////////////////////////////////////////////////////////////////// + // + // Invalid units + // + + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1 a", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1 ab", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "1 abc", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "0 m1", defaultUnits ); + impl_test_one_invalid_parse( unitsTypeMetaData, whiteSpace, /* invalidValue */ "0 1m", defaultUnits ); + } + } +} + +static Duration g_invalidDuration = (Duration){ .valueInUnits = INT64_MIN, .units = numberOfDurationUnits }; +ELASTIC_APM_DEFINE_VALUE_WITH_UNITS_TYPE_META_DATA( Duration, duration, ELASTIC_APM_UNKNOWN_DURATION_UNITS_AS_STRING, &g_invalidDuration ); +static Size g_invalidSize = (Size){ .valueInUnits = INT64_MIN, .units = numberOfSizeUnits }; +ELASTIC_APM_DEFINE_VALUE_WITH_UNITS_TYPE_META_DATA( Size, size, ELASTIC_APM_UNKNOWN_SIZE_UNITS_AS_STRING, &g_invalidSize ); + +static +void test_isValidDurationUnits( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + impl_test_isValidUnits( ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( duration ) ); +} + +static +void test_isValidSizeUnits( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + impl_test_isValidUnits( ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( size ) ); +} + +static +void test_durationUnitsToString( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + impl_test_unitsToString( ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( duration ) ); +} + +static +void test_sizeUnitsToString( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + impl_test_unitsToString( ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( size ) ); +} + +static +void test_parseDuration( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + impl_test_parse( ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( duration ) ); +} + +static +void test_parseSize( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + impl_test_parse( ELASTIC_APM_UNITS_META_DATA_GLOBAL_VAR_NAME( size ) ); +} + +int run_parse_value_with_units_tests() +{ + const struct CMUnitTest tests [] = + { + ELASTIC_APM_CMOCKA_UNIT_TEST( test_isValidSizeUnits ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_isValidDurationUnits ), + + ELASTIC_APM_CMOCKA_UNIT_TEST( test_durationUnitsToString ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_sizeUnitsToString ), + + ELASTIC_APM_CMOCKA_UNIT_TEST( test_parseDuration ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_parseSize ), + }; + + return cmocka_run_group_tests( tests, NULL, NULL ); +} diff --git a/src/ext/unit_tests/time_util_tests.c b/src/ext/unit_tests/time_util_tests.c index 858c937b0..d3c473e94 100644 --- a/src/ext/unit_tests/time_util_tests.c +++ b/src/ext/unit_tests/time_util_tests.c @@ -110,12 +110,55 @@ void test_calcTimeValDiff( void** testFixtureState ) } } +static +void impl_test_one_durationToMilliseconds( Duration inputDuration, Int64 expectedDurationInMilliseconds ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + Int64 actualDurationInMilliseconds = durationToMilliseconds( inputDuration ); + ELASTIC_APM_CMOCKA_ASSERT_MSG( + actualDurationInMilliseconds == expectedDurationInMilliseconds + , "inputDuration: %s, expectedDurationInMilliseconds: %"PRId64", actualDurationInMilliseconds: %"PRId64 + , streamDuration( inputDuration, &txtOutStream ), expectedDurationInMilliseconds, actualDurationInMilliseconds ); +} + +static +void test_durationToMilliseconds( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + // Test zero + ELASTIC_APM_FOR_EACH_INDEX( i, numberOfDurationUnits ) + { + impl_test_one_durationToMilliseconds( makeDuration( 0, (DurationUnits)i ), /* expectedDurationInMilliseconds */ 0 ); + } + + Int64 factor[ numberOfDurationUnits ] = + { + [ durationUnits_millisecond ] = 1, + [ durationUnits_second ] = 1000, + [ durationUnits_minute ] = 60 * 1000, + }; + Int64 valueInUnitsVariants[] = { 1, -1, 123, -4567890, INT8_MAX, INT8_MIN, INT16_MAX, INT16_MIN, INT32_MAX, INT32_MIN }; + ELASTIC_APM_FOR_EACH_INDEX( valueVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( valueInUnitsVariants ) ) + { + Int64 valueInUnits = valueInUnitsVariants[ valueVariantIndex ]; + ELASTIC_APM_FOR_EACH_INDEX( i, numberOfDurationUnits ) + { + Int64 expectedDurationInMilliseconds = valueInUnits * factor[ i ]; + impl_test_one_durationToMilliseconds( makeDuration( valueInUnits, (DurationUnits)i ), expectedDurationInMilliseconds ); + } + } +} + int run_time_util_tests( int argc, const char* argv[] ) { const struct CMUnitTest tests [] = { ELASTIC_APM_CMOCKA_UNIT_TEST( test_calcEndTimeVal ), ELASTIC_APM_CMOCKA_UNIT_TEST( test_calcTimeValDiff ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_durationToMilliseconds ), }; return cmocka_run_group_tests( tests, NULL, NULL ); diff --git a/src/ext/unit_tests/unit_test_util.h b/src/ext/unit_tests/unit_test_util.h index 1947600e9..2b46cc8d3 100644 --- a/src/ext/unit_tests/unit_test_util.h +++ b/src/ext/unit_tests/unit_test_util.h @@ -128,14 +128,21 @@ void elasticApmCmockaAssertStringContainsIgnoreCase( String haystack, String nee ELASTIC_APM_CMOCKA_ASSERT_INT_EQUAL( callAssertResultCode, resultSuccess ); \ } while ( 0 ) - -#define ELASTIC_APM_CMOCKA_ASSERT( cond ) assert_true( cond ) #define ELASTIC_APM_CMOCKA_ASSERT_VALID_PTR( ptr ) assert_ptr_not_equal( (ptr), NULL ) #define ELASTIC_APM_CMOCKA_ASSERT_NULL_PTR( ptr ) assert_ptr_equal( (ptr), NULL ) #define ELASTIC_APM_CMOCKA_FAIL_MSG( fmt, ... ) ELASTIC_APM_CMOCKA_ASSERT_FAILED( __FILE__, __LINE__, fmt, ##__VA_ARGS__ ) #define ELASTIC_APM_CMOCKA_FAIL() ELASTIC_APM_CMOCKA_FAIL_MSG( "" ) +#define ELASTIC_APM_CMOCKA_ASSERT( cond ) assert_true( cond ) +#define ELASTIC_APM_CMOCKA_ASSERT_MSG( cond, fmt, ... ) \ + do { \ + if ( ! (cond) ) \ + { \ + ELASTIC_APM_CMOCKA_FAIL_MSG( "Condition %s failed; " fmt, ELASTIC_APM_PP_STRINGIZE( cond ), ##__VA_ARGS__ ); \ + } \ + } while ( 0 ) + #define ELASTIC_APM_CMOCKA_UNIT_TEST_EX( func, setupFunc, teardownFunc, initialState ) \ ( struct CMUnitTest ) \ { \ diff --git a/src/ext/unit_tests/util_tests.c b/src/ext/unit_tests/util_tests.c index f0fa9a6e2..bf7f09c75 100644 --- a/src/ext/unit_tests/util_tests.c +++ b/src/ext/unit_tests/util_tests.c @@ -19,7 +19,8 @@ #include "cmocka_wrapped_for_unit_tests.h" #include "unit_test_util.h" - +#include "TextOutputStream.h" +#include static void areStringViewsEqual_test( void** testFixtureState ) @@ -81,6 +82,150 @@ static void trim_StringView_test( void** testFixtureState ) ELASTIC_APM_CMOCKA_ASSERT_STRING_VIEW_EQUAL_LITERAL( trimStringView( ELASTIC_APM_STRING_LITERAL_TO_VIEW( " \n\r\t" ) ), "" ); } +static void impl_test_one_parseDecimalInteger( String inputString, const Int64* expected ) +{ + ResultCode expectedResultCode = expected == NULL ? resultParsingFailed : resultSuccess; + Int64 dummy = INT64_MAX; + Int64 actual = dummy; + ResultCode actualResultCode = parseDecimalInteger( stringToView( inputString ), &actual ); + ELASTIC_APM_CMOCKA_ASSERT_MSG( + actualResultCode == expectedResultCode + , "expectedResultCode: %s (%d), parseDurationResultCode: %s (%d), inputString: `%s'" + , resultCodeToString( expectedResultCode ), (int)expectedResultCode + , resultCodeToString( actualResultCode ), (int)actualResultCode + , inputString ); + if ( expected == NULL ) + { + ELASTIC_APM_CMOCKA_ASSERT_INT_EQUAL( dummy, actual ); + } + else + { + ELASTIC_APM_CMOCKA_ASSERT_INT_EQUAL( *expected, actual ); + } +} + +static void impl_test_one_valid_parseDecimalInteger( String whiteSpace, Int64 expected ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String inputString = streamPrintf( &txtOutStream, "%s%"PRId64"%s", whiteSpace, expected, whiteSpace ); + + impl_test_one_parseDecimalInteger( inputString, &expected ); +} + +static void impl_test_one_invalid_parseDecimalInteger( String whiteSpace, String invalidValue ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String inputString = streamPrintf( &txtOutStream, "%s%s%s", whiteSpace, invalidValue, whiteSpace ); + + impl_test_one_parseDecimalInteger( inputString, /* expected */ NULL ); +} + +static void test_parseDecimalInteger( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + Int64 validValueVariants[] = { 0, 1, -1, 234, -567, INT_MAX, INT_MIN }; + String validWhiteSpaceVariants[] = { "", " ", " ", "\t", " \t " }; + + /////////////////////////////////////////////////////////////////// + // + // Valid + // + + ELASTIC_APM_FOR_EACH_INDEX( valueVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( validValueVariants ) ) + { + Int64 value = validValueVariants[ valueVariantIndex ]; + ELASTIC_APM_FOR_EACH_INDEX( whiteSpaceVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( validWhiteSpaceVariants ) ) + { + String whiteSpace = validWhiteSpaceVariants[ whiteSpaceVariantIndex ]; + impl_test_one_valid_parseDecimalInteger( whiteSpace, value ); + } + } + + /////////////////////////////////////////////////////////////////// + // + // Invalid + // + + ELASTIC_APM_FOR_EACH_INDEX( whiteSpaceVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( validWhiteSpaceVariants ) ) + { + String whiteSpace = validWhiteSpaceVariants[ whiteSpaceVariantIndex ]; + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "" ); + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "a" ); + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "z" ); + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "abc" ); + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "0x1" ); + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "0x" ); + impl_test_one_invalid_parseDecimalInteger( whiteSpace, "x0" ); + } +} + +static +void test_sizeUnitsToString( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + ELASTIC_APM_FOR_EACH_INDEX( sizeUnits, numberOfSizeUnits ) + { + ELASTIC_APM_CMOCKA_ASSERT_STRING_EQUAL( sizeUnitsNames[ sizeUnits ].begin, sizeUnitsToString( sizeUnits ), "sizeUnits: %d", (int)sizeUnits ); + } + + int valuesUnknownSizeUnits[] = { -1, INT_MIN, numberOfSizeUnits, numberOfSizeUnits + 1, numberOfSizeUnits + 10, 2 * numberOfSizeUnits }; + ELASTIC_APM_FOR_EACH_INDEX( i, ELASTIC_APM_STATIC_ARRAY_SIZE( valuesUnknownSizeUnits ) ) + { + int value = valuesUnknownSizeUnits[ i ]; + String valueAsSizeUnitsString = sizeUnitsToString( value ); + ELASTIC_APM_CMOCKA_ASSERT_STRING_EQUAL( ELASTIC_APM_UNKNOWN_SIZE_UNITS_AS_STRING, valueAsSizeUnitsString, "i: %d, value: %d", (int)i, value ); + } +} + +static +void impl_test_one_sizeToBytes( Size inputSize, Int64 expectedSizeInBytes ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + Int64 actualSizeInBytes = sizeToBytes( inputSize ); + ELASTIC_APM_CMOCKA_ASSERT_MSG( + actualSizeInBytes == expectedSizeInBytes + , "inputSize: %s, expectedSizeInBytes: %"PRId64", actualSizeInBytes: %"PRId64 + , streamSize( inputSize, &txtOutStream ), expectedSizeInBytes, actualSizeInBytes ); +} + +static +void test_sizeToBytes( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + // Test zero + ELASTIC_APM_FOR_EACH_INDEX( i, numberOfSizeUnits ) + { + impl_test_one_sizeToBytes( makeSize( 0, (SizeUnits)i ), /* expectedSizeInBytes */ 0 ); + } + + Int64 factor[ numberOfSizeUnits ] = + { + [ sizeUnits_byte ] = 1, + [ sizeUnits_kibibyte ] = 1024, + [ sizeUnits_mebibyte ] = 1024 * 1024, + [ sizeUnits_gibibyte ] = 1024 * 1024 * 1024, + }; + Int64 valueInUnitsVariants[] = { 1, 123, 4567890, INT8_MAX, UINT8_MAX, INT16_MAX, UINT16_MAX, INT32_MAX, UINT32_MAX }; + ELASTIC_APM_FOR_EACH_INDEX( valueVariantIndex, ELASTIC_APM_STATIC_ARRAY_SIZE( valueInUnitsVariants ) ) + { + Int64 valueInUnits = valueInUnitsVariants[ valueVariantIndex ]; + ELASTIC_APM_FOR_EACH_INDEX( i, numberOfSizeUnits ) + { + Int64 expectedSizeInBytes = valueInUnits * factor[ i ]; + impl_test_one_sizeToBytes( makeSize( valueInUnits, (SizeUnits)i ), expectedSizeInBytes ); + } + } +} + int run_util_tests() { const struct CMUnitTest tests [] = @@ -89,6 +234,9 @@ int run_util_tests() ELASTIC_APM_CMOCKA_UNIT_TEST( areStringsEqualIgnoringCase_test ), ELASTIC_APM_CMOCKA_UNIT_TEST( calcAlignedSize_test ), ELASTIC_APM_CMOCKA_UNIT_TEST( trim_StringView_test ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_parseDecimalInteger ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_sizeUnitsToString ), + ELASTIC_APM_CMOCKA_UNIT_TEST( test_sizeToBytes ), }; return cmocka_run_group_tests( tests, NULL, NULL ); diff --git a/src/ext/util.c b/src/ext/util.c index 10f78a3ce..01da75be2 100644 --- a/src/ext/util.c +++ b/src/ext/util.c @@ -18,6 +18,8 @@ */ #include "util.h" +#include +#include "TextOutputStream.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_UTIL @@ -57,3 +59,140 @@ StringView findEndOfLineSequence( StringView text ) return makeEmptyStringView(); } + +bool findCharByPredicate( StringView src, CharPredicate predicate, size_t* foundPosition ) +{ + ELASTIC_APM_FOR_EACH_INDEX( pos, src.length ) + { + if ( predicate( src.begin[ pos ] ) ) + { + *foundPosition = pos; + return true; + } + } + + return false; +} + +ResultCode parseDecimalInteger( StringView inputString, /* out */ Int64* result ) +{ + char* pastLastInterpretedChar = NULL; + StringView trimmedInputString = trimStringView( inputString ); + if ( isEmptyStringView( trimmedInputString ) ) + { + return resultParsingFailed; + } + + long parsedNumber = strtol( trimmedInputString.begin, /* out */ &pastLastInterpretedChar, /* base */ 10 ); + if ( pastLastInterpretedChar != stringViewEnd( trimmedInputString ) ) + { + return resultParsingFailed; + } + *result = parsedNumber; + return resultSuccess; +} + +static +ResultCode parseUnits( StringView inputString, StringView unitNames[], size_t numberOfUnits, /* out */ size_t* unitsIndex ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( unitsIndex ); + + StringView trimmedInputString = trimStringView( inputString ); + + ELASTIC_APM_FOR_EACH_INDEX( i, numberOfUnits ) + { + if ( areStringViewsEqualIgnoringCase( trimmedInputString, unitNames[ i ] ) ) + { + *unitsIndex = i; + return resultSuccess; + } + } + + return resultParsingFailed; +} + +ResultCode parseDecimalIntegerWithUnits( StringView inputString, StringView unitNames[], size_t numberOfUnits, /* out */ Int64* valueInUnits, /* out */ size_t* unitsIndex ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( valueInUnits ); + ELASTIC_APM_ASSERT_VALID_PTR( unitsIndex ); + + ResultCode resultCode; + StringView trimmedInputString = trimStringView( inputString ); + size_t firstNotDecimalDigit; + size_t unitsIndexLocal = numberOfUnits; + StringView numericPart = trimmedInputString; + Int64 numericValue; + + if ( findCharByPredicate( trimmedInputString, isNotDecimalDigitOrSign, /* out */ &firstNotDecimalDigit ) ) + { + ELASTIC_APM_CALL_IF_FAILED_GOTO( parseUnits( subStringView( trimmedInputString, /* offset */ firstNotDecimalDigit ), unitNames, numberOfUnits, /* out */ &unitsIndexLocal ) ); + numericPart = trimStringView( makeStringView( trimmedInputString.begin, firstNotDecimalDigit ) ); + } + else + { + unitsIndexLocal = numberOfUnits; + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( parseDecimalInteger( numericPart, /* out */ &numericValue ) ); + + *valueInUnits = numericValue; + *unitsIndex = unitsIndexLocal; + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +StringView sizeUnitsNames[ numberOfSizeUnits ] = +{ + [ sizeUnits_byte ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "B" ), + [ sizeUnits_kibibyte ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "KB" ), + [ sizeUnits_mebibyte ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "MB" ), + [ sizeUnits_gibibyte ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "GB" ), +}; + +ResultCode parseSize( StringView inputString, SizeUnits defaultUnits, /* out */ Size* result ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( result ); + + size_t unitsIndex; + ResultCode resultCode = parseDecimalIntegerWithUnits( inputString, sizeUnitsNames, numberOfSizeUnits, &result->valueInUnits, &unitsIndex ); + if ( resultCode == resultSuccess ) + { + result->units = ( unitsIndex == numberOfSizeUnits ? defaultUnits : (SizeUnits)unitsIndex ); + } + return resultCode; +} + +String streamSize( Size size, TextOutputStream* txtOutStream ) +{ + return isValidSizeUnits( size.units ) + ? streamPrintf( txtOutStream, "%"PRId64"%s", size.valueInUnits, sizeUnitsToString( size.units ) ) + : streamPrintf( txtOutStream, "%"PRId64"", size.valueInUnits, size.units ); +} + +static const Int64 sizeKibiFactor = 1024; + +Int64 sizeToBytes( Size size ) +{ + switch (size.units) + { + case sizeUnits_byte: + return size.valueInUnits; + + case sizeUnits_kibibyte: + return size.valueInUnits * sizeKibiFactor; + + case sizeUnits_mebibyte: + return size.valueInUnits * sizeKibiFactor * sizeKibiFactor; + + case sizeUnits_gibibyte: + return size.valueInUnits * sizeKibiFactor * sizeKibiFactor * sizeKibiFactor; + + default: + ELASTIC_APM_ASSERT( false, "Unknown size units (as int): %d", size.units ); + return size.valueInUnits; + } +} diff --git a/src/ext/util.h b/src/ext/util.h index 4d07964d7..cc9485515 100644 --- a/src/ext/util.h +++ b/src/ext/util.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "constants.h" #include "basic_types.h" #include "StringView.h" @@ -88,6 +89,16 @@ static inline char charToUpperCase( char c ) return c; } +static inline char charToLowerCase( char c ) +{ + if ( ELASTIC_APM_IS_IN_INCLUSIVE_RANGE( 'A', c, 'Z' ) ) + { + return (char) ( (char) ( c - 'A' ) + 'a' ); + } + + return c; +} + static inline void copyStringAsUpperCase( String src, /* out */ MutableString dst ) { size_t i = 0; @@ -106,11 +117,18 @@ bool isStringViewPrefixIgnoringCase( StringView str, StringView prefix ) ELASTIC_APM_ASSERT_VALID_STRING_VIEW( str ); ELASTIC_APM_ASSERT_VALID_STRING_VIEW( prefix ); - if ( prefix.length > str.length ) return false; + if ( prefix.length > str.length ) + { + return false; + } ELASTIC_APM_FOR_EACH_INDEX( i, prefix.length ) + { if ( ! areCharsEqualIgnoringCase( str.begin[ i ], prefix.begin[ i ] ) ) + { return false; + } + } return true; } @@ -122,22 +140,56 @@ bool areStringsEqualIgnoringCase( String str1, String str2 ) ELASTIC_APM_ASSERT_VALID_STRING( str2 ); for ( const char *p1 = str1, *p2 = str2; areCharsEqualIgnoringCase( *p1, *p2 ) ; ++p1, ++p2 ) - if ( *p1 == '\0' ) return true; + { + if ( *p1 == '\0' ) + { + return true; + } + } return false; } +static inline +bool areStringViewsEqualIgnoringCase( StringView strVw1, StringView strVw2 ) +{ + ELASTIC_APM_ASSERT_VALID_STRING_VIEW( strVw1 ); + ELASTIC_APM_ASSERT_VALID_STRING_VIEW( strVw2 ); + + if ( strVw1.length != strVw2.length ) + { + return false; + } + + ELASTIC_APM_FOR_EACH_INDEX( i, strVw1.length ) + { + if ( ! areCharsEqualIgnoringCase( strVw1.begin[ i ], strVw2.begin[ i ] ) ) + { + return false; + } + } + + return true; +} + static inline bool areStringViewsEqual( StringView strVw1, StringView strVw2 ) { ELASTIC_APM_ASSERT_VALID_STRING_VIEW( strVw1 ); ELASTIC_APM_ASSERT_VALID_STRING_VIEW( strVw2 ); - if ( strVw1.length != strVw2.length ) return false; + if ( strVw1.length != strVw2.length ) + { + return false; + } ELASTIC_APM_FOR_EACH_INDEX( i, strVw1.length ) + { if ( strVw1.begin[ i ] != strVw2.begin[ i ] ) + { return false; + } + } return true; } @@ -245,3 +297,101 @@ bool isWhiteSpace( char c ) StringView trimStringView( StringView src ); StringView findEndOfLineSequence( StringView text ); + +static inline +bool isDecimalDigit( char c ) +{ + return isdigit( c ) != 0; +} + +typedef bool (* CharPredicate)( char c ); + +bool findCharByPredicate( StringView src, CharPredicate predicate, size_t* foundPosition ); + +static inline +bool isDecimalDigitOrSign( char c ) +{ + return ( c == '-' ) || isDecimalDigit( c ); +} + +static inline +bool isNotDecimalDigitOrSign( char c ) +{ + return ! isDecimalDigitOrSign( c ); +} + +ResultCode parseDecimalInteger( StringView inputString, /* out */ Int64* result ); +ResultCode parseDecimalIntegerWithUnits( StringView inputString, StringView unitNames[], size_t numberOfUnits, /* out */ Int64* valueInUnits, /* out */ size_t* unitsIndex ); + +static inline +bool ifThen( bool ifCond, bool thenCond ) +{ + return ( ! ifCond ) || thenCond; +} + +enum SizeUnits +{ + sizeUnits_byte, + sizeUnits_kibibyte, + sizeUnits_mebibyte, + sizeUnits_gibibyte, + + numberOfSizeUnits +}; +typedef enum SizeUnits SizeUnits; +extern StringView sizeUnitsNames[ numberOfSizeUnits ]; + +struct Size +{ + Int64 valueInUnits; + SizeUnits units; +}; +typedef struct Size Size; + +static inline +bool isValidSizeUnits( SizeUnits units ) +{ + return ( sizeUnits_byte <= units ) && ( units < numberOfSizeUnits ); +} + +#define ELASTIC_APM_UNKNOWN_SIZE_UNITS_AS_STRING "" + +static inline +String sizeUnitsToString( SizeUnits sizeUnits ) +{ + if ( isValidSizeUnits( sizeUnits ) ) + { + return sizeUnitsNames[ sizeUnits ].begin; + } + return ELASTIC_APM_UNKNOWN_SIZE_UNITS_AS_STRING; +} + +static inline +Size makeSize( Int64 valueInUnits, SizeUnits units ) +{ + return (Size){ .valueInUnits = valueInUnits, .units = units }; +} + +ResultCode parseSize( StringView inputString, SizeUnits defaultUnits, /* out */ Size* result ); + +String streamSize( Size size, TextOutputStream* txtOutStream ); + +Int64 sizeToBytes( Size size ); + +#define ELASTIC_APM_DEFINE_ARRAY_VIEW_EX( ElementType, ViewTypeName ) \ + struct ViewTypeName \ + { \ + ElementType* values; \ + size_t size; \ + }; \ + typedef struct ViewTypeName ViewTypeName + +#define ELASTIC_APM_DEFINE_ARRAY_VIEW( ElementType ) ELASTIC_APM_DEFINE_ARRAY_VIEW_EX( ElementType, ELASTIC_APM_PP_CONCAT( ElementType, ArrayView ) ) + +#define ELASTIC_APM_STATIC_ARRAY_TO_VIEW( ViewTypeName, staticArray ) ((ViewTypeName){ .values = &((staticArray)[0]), .size = ELASTIC_APM_STATIC_ARRAY_SIZE( staticArray ) }) + +#define ELASTIC_APM_EMPTY_ARRAY_VIEW( ViewTypeName ) ((ViewTypeName){ .values = NULL, .size = 0 }) + +ELASTIC_APM_DEFINE_ARRAY_VIEW_EX( int, IntArrayView ); +ELASTIC_APM_DEFINE_ARRAY_VIEW( StringView ); +ELASTIC_APM_DEFINE_ARRAY_VIEW( Int64 ); diff --git a/tests/ElasticApmTests/ComponentTests/ApiKeySecretTokenTest.php b/tests/ElasticApmTests/ComponentTests/ApiKeySecretTokenTest.php index 668e3ea70..97037de89 100644 --- a/tests/ElasticApmTests/ComponentTests/ApiKeySecretTokenTest.php +++ b/tests/ElasticApmTests/ComponentTests/ApiKeySecretTokenTest.php @@ -49,7 +49,7 @@ function (AppCodeHostParams $appCodeParams) use ($configuredApiKey, $configuredS ); $appCodeHost->sendRequest(AppCodeTarget::asRouted([__CLASS__, 'appCodeEmpty'])); $dataFromAgent = $this->waitForOneEmptyTransaction($testCaseHandle); - foreach ($dataFromAgent->intakeApiRequests as $intakeApiRequest) { + foreach ($dataFromAgent->getAllIntakeApiRequests() as $intakeApiRequest) { DataFromAgentPlusRawValidator::verifyAuthIntakeApiHttpRequestHeader( $configuredApiKey, $configuredSecretToken, diff --git a/tests/ElasticApmTests/ComponentTests/BackendCommTest.php b/tests/ElasticApmTests/ComponentTests/BackendCommTest.php new file mode 100644 index 000000000..6331f929d --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/BackendCommTest.php @@ -0,0 +1,77 @@ + $appCodeArgs + */ + public static function appCodeForTestNumberOfConnections(array $appCodeArgs): void + { + $txName = self::getStringFromMap('txName', $appCodeArgs); + ElasticApm::getCurrentTransaction()->setName($txName); + } + + public function testNumberOfConnections(): void + { + if (self::skipIfMainAppCodeHostIsNotHttp()) { + return; + } + $testCaseHandle = $this->getTestCaseHandle(); + $appCodeHost = $testCaseHandle->ensureMainAppCodeHost(); + $txNames = ['1st_test_TX', '2nd_test_TX']; + foreach ($txNames as $txName) { + $appCodeHost->sendRequest( + AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestNumberOfConnections']), + function (AppCodeRequestParams $appCodeRequestParams) use ($txName): void { + $appCodeRequestParams->setAppCodeArgs(['txName' => $txName]); + $appCodeRequestParams->expectedTransactionName->setValue($txName); + self::assertInstanceOf(HttpAppCodeRequestParams::class, $appCodeRequestParams); + } + ); + } + $txCount = count($txNames); + $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions($txCount)); + $msg = new AssertMessageBuilder(['connections' => $dataFromAgent->getRaw()->getIntakeApiConnections()]); + self::assertCount(1, $dataFromAgent->getRaw()->getIntakeApiConnections(), $msg->s()); + $txIndex = 0; + foreach ($dataFromAgent->idToTransaction as $tx) { + self::assertSame($txNames[$txIndex], $tx->name); + ++$txIndex; + } + } +} diff --git a/tests/ElasticApmTests/ComponentTests/CurlAutoInstrumentationTest.php b/tests/ElasticApmTests/ComponentTests/CurlAutoInstrumentationTest.php index 586f181a6..217d68fed 100644 --- a/tests/ElasticApmTests/ComponentTests/CurlAutoInstrumentationTest.php +++ b/tests/ElasticApmTests/ComponentTests/CurlAutoInstrumentationTest.php @@ -116,9 +116,10 @@ function (AppCodeRequestParams $reqParams) use ($serverAppCode): void { $dataPerRequest = $serverAppCode->buildDataPerRequest( AppCodeTarget::asRouted([__CLASS__, 'appCodeServer']) ); + $additionalAppCodeHostPort = $serverAppCode->getHttpServerHandle()->getMainPort(); $reqParams->setAppCodeArgs( [ - self::SERVER_PORT_KEY => $serverAppCode->getPort(), + self::SERVER_PORT_KEY => $additionalAppCodeHostPort, self::DATA_PER_REQUEST_FOR_SERVER_SIDE_KEY => $dataPerRequest->serializeToString(), ] ); diff --git a/tests/ElasticApmTests/ComponentTests/HttpTransactionTest.php b/tests/ElasticApmTests/ComponentTests/HttpTransactionTest.php index 4ee88c0b0..2fff22857 100644 --- a/tests/ElasticApmTests/ComponentTests/HttpTransactionTest.php +++ b/tests/ElasticApmTests/ComponentTests/HttpTransactionTest.php @@ -348,6 +348,7 @@ function (AppCodeHostParams $appCodeParams) use ($urlGroupsConfigVal): void { function (AppCodeRequestParams $appCodeRequestParams) use ($urlParts, $expectedTxName): void { $appCodeRequestParams->expectedTransactionName->setValue($expectedTxName); self::assertInstanceOf(HttpAppCodeRequestParams::class, $appCodeRequestParams); + /** @var HttpAppCodeRequestParams $appCodeRequestParams */ $appCodeRequestParams->urlParts->path($urlParts->path)->query($urlParts->query); } ); diff --git a/tests/ElasticApmTests/ComponentTests/UserAgentTest.php b/tests/ElasticApmTests/ComponentTests/UserAgentTest.php index 41cdf6882..b73044242 100644 --- a/tests/ElasticApmTests/ComponentTests/UserAgentTest.php +++ b/tests/ElasticApmTests/ComponentTests/UserAgentTest.php @@ -121,7 +121,7 @@ function (AppCodeHostParams $appCodeParams) use ($configuredServiceName, $config ); $appCodeHost->sendRequest(AppCodeTarget::asRouted([__CLASS__, 'appCodeEmpty'])); $dataFromAgent = $this->waitForOneEmptyTransaction($testCaseHandle); - foreach ($dataFromAgent->intakeApiRequests as $intakeApiRequest) { + foreach ($dataFromAgent->getAllIntakeApiRequests() as $intakeApiRequest) { DataFromAgentPlusRawValidator::verifyUserAgentIntakeApiHttpRequestHeader( $expectedUserAgentHeaderValue, $intakeApiRequest->headers diff --git a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostBase.php b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostBase.php index db48ab80e..318d00cc2 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostBase.php @@ -83,6 +83,11 @@ function (SpawnedProcessBase $thisObj) use (&$topLevelCodeId): void { ); } + protected function isThisProcessTestScoped(): bool + { + return true; + } + protected function registerWithResourcesCleaner(): void { // We don't want any of the infrastructure operations to be recorded as application's APM events diff --git a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostHandle.php b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostHandle.php index 326f0317a..eb5c8e63c 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostHandle.php @@ -70,7 +70,7 @@ protected function beforeAppCodeInvocation(AppCodeRequestParams $appCodeRequestP protected function afterAppCodeInvocation(AppCodeInvocation $appCodeInvocation): void { $appCodeInvocation->after(); - $this->testCaseHandle->setAppCodeInvocation($appCodeInvocation); + $this->testCaseHandle->addAppCodeInvocation($appCodeInvocation); } /** diff --git a/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHost.php b/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHost.php index 755b17292..eed53d8ec 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHost.php +++ b/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHost.php @@ -73,8 +73,9 @@ protected function shouldRegisterThisProcessWithResourcesCleaner(): bool protected function processConfig(): void { - TestCase::assertNotNull( - AmbientContextForTests::testConfig()->dataPerProcess->thisServerPort, + TestCase::assertCount( + 1, + AmbientContextForTests::testConfig()->dataPerProcess->thisServerPorts, LoggableToString::convert(AmbientContextForTests::testConfig()) ); diff --git a/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHostStarter.php b/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHostStarter.php index 39f8c44e1..e327f9fdb 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHostStarter.php +++ b/tests/ElasticApmTests/ComponentTests/Util/BuiltinHttpServerAppCodeHostStarter.php @@ -24,6 +24,7 @@ namespace ElasticApmTests\ComponentTests\Util; use ElasticApmTests\Util\FileUtilForTests; +use PHPUnit\Framework\Assert; final class BuiltinHttpServerAppCodeHostStarter extends HttpServerStarter { @@ -69,20 +70,22 @@ public static function startBuiltinHttpServerAppCodeHost( } /** @inheritDoc */ - protected function buildCommandLine(int $port): string + protected function buildCommandLine(array $ports): string { + Assert::assertCount(1, $ports); return InfraUtilForTests::buildAppCodePhpCmd($this->agentConfigSourceBuilder->getPhpIniFile()) - . " -S localhost:$port" + . " -S localhost:" . $ports[0] . ' "' . FileUtilForTests::listToPath([__DIR__, self::APP_CODE_HOST_ROUTER_SCRIPT]) . '"'; } /** @inheritDoc */ - protected function buildEnvVars(string $spawnedProcessInternalId, int $port): array + protected function buildEnvVars(string $spawnedProcessInternalId, array $ports): array { + Assert::assertCount(1, $ports); return InfraUtilForTests::addTestInfraDataPerProcessToEnvVars( $this->agentConfigSourceBuilder->getEnvVars(EnvVarUtilForTests::getAll()), $spawnedProcessInternalId, - $port, + $ports, $this->resourcesCleaner, $this->appCodeHostParams->dbgProcessName ); diff --git a/tests/ElasticApmTests/ComponentTests/Util/CliScriptAppCodeHostHandle.php b/tests/ElasticApmTests/ComponentTests/Util/CliScriptAppCodeHostHandle.php index 786fbc9f3..19dff26bd 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/CliScriptAppCodeHostHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/CliScriptAppCodeHostHandle.php @@ -89,7 +89,7 @@ public function sendRequest(AppCodeTarget $appCodeTarget, ?Closure $setParamsFun $envVars = InfraUtilForTests::addTestInfraDataPerProcessToEnvVars( $this->agentConfigSourceBuilder->getEnvVars(EnvVarUtilForTests::getAll()), $this->appCodeHostParams->spawnedProcessInternalId, - null /* <- targetServerPort */, + [] /* <- targetServerPorts */, $this->resourcesCleaner, $this->appCodeHostParams->dbgProcessName ); diff --git a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php index be585a674..e66fa9966 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php @@ -174,6 +174,19 @@ protected static function getIntFromMap(string $argKey, array $argsMap): int return $val; } + /** + * @param string $argKey + * @param array $argsMap + * + * @return string + */ + protected static function getStringFromMap(string $argKey, array $argsMap): string + { + $val = self::getFromMap($argKey, $argsMap); + self::assertIsString($val, LoggableToString::convert(['argKey' => $argKey, 'argsMap' => $argsMap])); + return $val; + } + /** * @param string $argKey * @param array $argsMap @@ -362,7 +375,7 @@ private function runAndEscalateLogLevelOnFailureImpl(string $dbgTestDesc, callab $initiallyFailedTestLogLevels = $this->getCurrentLogLevels($this->testCaseHandle); $logger->addAllContext( [ - 'initiallyFailedTestLogLevels' => $initiallyFailedTestLogLevels, + 'initiallyFailedTestLogLevels' => self::logLevelsWithNames($initiallyFailedTestLogLevels), 'initiallyFailedTestException' => $initiallyFailedTestException, ] ); @@ -377,7 +390,7 @@ private function runAndEscalateLogLevelOnFailureImpl(string $dbgTestDesc, callab ++$rerunCount; $loggerPerIteration = $logger->inherit()->addAllContext( - ['rerunCount' => $rerunCount, 'escalatedLogLevels' => $escalatedLogLevels] + ['rerunCount' => $rerunCount, 'escalatedLogLevels' => self::logLevelsWithNames($escalatedLogLevels)] ); ($loggerProxy = $loggerPerIteration->ifInfoLevelEnabled(__LINE__, __FUNCTION__)) @@ -516,4 +529,18 @@ protected static function generateLevelsForRunAndEscalateLogLevelOnFailure( $result = IterableUtilForTests::duplicateEachElement($result, $eachLevelsSetMaxCount); return $result; } + + /** + * @param array $logLevels + * + * @return array + */ + protected static function logLevelsWithNames(array $logLevels): array + { + $result = []; + foreach ($logLevels as $levelTypeKey => $logLevel) { + $result[$levelTypeKey] = LogLevel::intToName($logLevel) . ' (' . $logLevel . ')'; + } + return $result; + } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestsPhpUnitExtension.php b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestsPhpUnitExtension.php index 5a9cbf4a2..02df4678a 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestsPhpUnitExtension.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestsPhpUnitExtension.php @@ -63,6 +63,9 @@ final class ComponentTestsPhpUnitExtension extends PhpUnitExtensionBase implemen /** @var Logger */ private $logger; + /** @var ?GlobalTestInfra */ + private static $globalTestInfra = null; + public function __construct() { parent::__construct(self::DBG_PROCESS_NAME); @@ -77,6 +80,14 @@ public function __construct() )->addContext('appCodeHostKind', AmbientContextForTests::testConfig()->appCodeHostKind()); } + public static function getGlobalTestInfra(): GlobalTestInfra + { + if (self::$globalTestInfra === null) { + self::$globalTestInfra = new GlobalTestInfra(); + } + return self::$globalTestInfra; + } + public static function initSingletons(): void { AmbientContextForTests::init(self::DBG_PROCESS_NAME); diff --git a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRaw.php b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRaw.php index d51667989..cc3d16f22 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRaw.php +++ b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRaw.php @@ -27,6 +27,24 @@ final class DataFromAgentPlusRaw extends DataFromAgent { - /** @var IntakeApiRequest[] */ - public $intakeApiRequests = []; + /** @var RawDataFromAgent */ + private $raw; + + public function __construct(RawDataFromAgent $raw) + { + $this->raw = $raw; + } + + public function getRaw(): RawDataFromAgent + { + return $this->raw; + } + + /** + * @return IntakeApiRequest[] + */ + public function getAllIntakeApiRequests(): array + { + return $this->raw->getAllIntakeApiRequests(); + } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawAccumulator.php b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawAccumulator.php index ad4c6036c..9681b6841 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawAccumulator.php +++ b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawAccumulator.php @@ -27,76 +27,117 @@ use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Log\LoggableTrait; use Elastic\Apm\Impl\Log\Logger; +use Elastic\Apm\Impl\Util\ArrayUtil; +use ElasticApmTests\Util\ArrayUtilForTests; +use ElasticApmTests\Util\DataFromAgent; use ElasticApmTests\Util\Deserialization\SerializedEventSinkTrait; use ElasticApmTests\Util\LogCategoryForTests; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; -final class DataFromAgentPlusRawAccumulator implements LoggableInterface +final class DataFromAgentPlusRawAccumulator implements RawDataFromAgentReceiverEventVisitorInterface, LoggableInterface { use LoggableTrait; use SerializedEventSinkTrait; - /** @var DataFromAgentPlusRaw */ - private $result; + /** @var IntakeApiConnection[] */ + private $closedIntakeApiConnections = []; + + /** @var bool */ + private $isOpenIntakeApiConnection = false; + + /** @var IntakeApiRequest[] */ + private $openIntakeApiConnectionRequests = []; + + /** @var DataFromAgent */ + private $dataParsed; /** @var Logger */ private $logger; public function __construct() { - $this->result = new DataFromAgentPlusRaw(); $this->logger = AmbientContextForTests::loggerFactory()->loggerForClass( LogCategoryForTests::TEST_UTIL, __NAMESPACE__, __CLASS__, __FILE__ )->addContext('this', $this); + + $this->dataParsed = new DataFromAgent(); } public function dbgCounts(): ExpectedEventCounts { return (new ExpectedEventCounts()) - ->errors(count($this->result->idToError)) - ->metricSets(count($this->result->metricSets)) - ->spans(count($this->result->idToSpan)) - ->transactions(count($this->result->idToTransaction)); + ->errors(count($this->dataParsed->idToError)) + ->metricSets(count($this->dataParsed->metricSets)) + ->spans(count($this->dataParsed->idToSpan)) + ->transactions(count($this->dataParsed->idToTransaction)); } /** - * @param IntakeApiRequest[] $intakeApiRequests + * @param RawDataFromAgentReceiverEvent[] $receiverEvents * * @return void */ - public function addIntakeApiRequests(array $intakeApiRequests): void + public function addReceiverEvents(array $receiverEvents): void { - foreach ($intakeApiRequests as $intakeApiRequest) { - $dataFromAgent = IntakeApiRequestDeserializer::deserialize($intakeApiRequest); - TestCase::assertCount(1, $dataFromAgent->metadatas); - $metadata = $dataFromAgent->metadatas[0]; - TestCase::assertNotNull($metadata->service->agent); - TestCase::assertNotNull( - $metadata->service->agent->ephemeralId, - LoggableToString::convert( - [ - '$intakeApiRequests' => $intakeApiRequests, - '$metadata' => $metadata, - ] - ) - ); - $intakeApiRequest->agentEphemeralId = $metadata->service->agent->ephemeralId; - $this->result->intakeApiRequests[] = $intakeApiRequest; - foreach (get_object_vars($dataFromAgent) as $propName => $propValue) { - TestCase::assertIsArray($propValue); - TestCase::assertIsArray($this->result->$propName); - $this->result->$propName = array_merge($this->result->$propName, $propValue); - } + foreach ($receiverEvents as $receiverEvent) { + $receiverEvent->visit($this); + } + } + + public function visitConnectionStarted(RawDataFromAgentReceiverEventConnectionStarted $event): void + { + $this->addNewConnection($event); + } + + public function visitRequest(RawDataFromAgentReceiverEventRequest $event): void + { + $this->addIntakeApiRequest($event->request); + } + + /** @noinspection PhpUnusedParameterInspection */ + private function addNewConnection(RawDataFromAgentReceiverEventConnectionStarted $event): void + { + if (!$this->isOpenIntakeApiConnection) { + $this->isOpenIntakeApiConnection = true; + Assert::assertCount(0, $this->openIntakeApiConnectionRequests); + return; + } + + $this->closedIntakeApiConnections[] = new IntakeApiConnection($this->openIntakeApiConnectionRequests); + $this->openIntakeApiConnectionRequests = []; + } + + private function addIntakeApiRequest(IntakeApiRequest $intakeApiRequest): void + { + $this->openIntakeApiConnectionRequests[] = $intakeApiRequest; + + $newDataParsed = IntakeApiRequestDeserializer::deserialize($intakeApiRequest); + TestCase::assertCount(1, $newDataParsed->metadatas); + $metadata = $newDataParsed->metadatas[0]; + TestCase::assertNotNull($metadata->service->agent); + $dbgCtx = ['intakeApiRequest' => $intakeApiRequest, '$metadata' => $metadata]; + TestCase::assertNotNull($metadata->service->agent->ephemeralId, LoggableToString::convert($dbgCtx)); + $intakeApiRequest->agentEphemeralId = $metadata->service->agent->ephemeralId; + $this->appendParsedData($newDataParsed, $this->dataParsed); + } + + private static function appendParsedData(DataFromAgent $from, DataFromAgent $to): void + { + foreach (get_object_vars($from) as $propName => $propValue) { + TestCase::assertIsArray($propValue); + TestCase::assertIsArray($to->$propName); + ArrayUtilForTests::append(/* from */ $propValue, /* to, ref */ $to->$propName); } } public function hasReachedEventCounts(ExpectedEventCounts $expectedEventCounts): bool { foreach (ApmDataKind::all() as $apmDataKind) { - $actualCount = $this->result->getApmDataCountForKind($apmDataKind); + $actualCount = $this->dataParsed->getApmDataCountForKind($apmDataKind); $ctx = [ '$apmDataKind' => $apmDataKind, '$expectedEventCounts' => $expectedEventCounts, @@ -119,6 +160,12 @@ public function hasReachedEventCounts(ExpectedEventCounts $expectedEventCounts): public function getAccumulatedData(): DataFromAgentPlusRaw { - return $this->result; + $intakeApiConnections = $this->closedIntakeApiConnections; + if (!ArrayUtil::isEmpty($this->openIntakeApiConnectionRequests)) { + $intakeApiConnections[] = new IntakeApiConnection($this->openIntakeApiConnectionRequests); + } + $result = new DataFromAgentPlusRaw(new RawDataFromAgent($intakeApiConnections)); + self::appendParsedData($this->dataParsed, $result); + return $result; } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawExpectations.php b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawExpectations.php index 1a2997a3e..9e6027940 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawExpectations.php +++ b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawExpectations.php @@ -33,43 +33,56 @@ use ElasticApmTests\Util\TestCaseBase; use ElasticApmTests\Util\TraceExpectations; use ElasticApmTests\Util\TransactionExpectations; +use PHPUnit\Framework\Assert; final class DataFromAgentPlusRawExpectations extends DataFromAgentExpectations { - /** @var AppCodeInvocation */ - public $appCodeInvocation; + /** @var AppCodeInvocation[] */ + public $appCodeInvocations; /** @var float */ - public $timeReceivedLastIntakeApiRequest; + public $timeAllDataReceivedAtApmServer; - public function __construct(AppCodeInvocation $appCodeInvocation, float $timeReceivedLastIntakeApiRequest) + /** + * @param AppCodeInvocation[] $appCodeInvocations + * @param float $timeAllDataReceivedAtApmServer + */ + public function __construct(array $appCodeInvocations, float $timeAllDataReceivedAtApmServer) { - $this->appCodeInvocation = $appCodeInvocation; - $this->timeReceivedLastIntakeApiRequest = $timeReceivedLastIntakeApiRequest; - $this->fillExpectations(); + $this->appCodeInvocations = $appCodeInvocations; + $this->timeAllDataReceivedAtApmServer = $timeAllDataReceivedAtApmServer; + $this->fillExpectationsFromParsedData(); } - private function fillExpectations(): void + private function fillExpectationsFromParsedData(): void + { + Assert::assertNotEmpty($this->appCodeInvocations); + foreach ($this->appCodeInvocations as $appCodeInvocation) { + $this->addExpectationsForAppCodeInvocation($appCodeInvocation); + } + } + + private function addExpectationsForAppCodeInvocation(AppCodeInvocation $appCodeInvocation): void { $transactionExpectations = new TransactionExpectations(); - $transactionExpectations->isSampled = $this->deriveIsSampledExpectation(); - $transactionExpectations->timestampBefore = $this->appCodeInvocation->timestampBefore; - $transactionExpectations->timestampAfter = $this->timeReceivedLastIntakeApiRequest; - if (!$this->appCodeInvocation->appCodeRequestParams->shouldAssumeNoDroppedSpans) { + $transactionExpectations->isSampled = self::deriveIsSampledExpectation($appCodeInvocation); + $transactionExpectations->timestampBefore = $appCodeInvocation->timestampBefore; + $transactionExpectations->timestampAfter = $this->timeAllDataReceivedAtApmServer; + if (!$appCodeInvocation->appCodeRequestParams->shouldAssumeNoDroppedSpans) { $transactionExpectations->droppedSpansCount = null; } - $this->fillErrorExpectations($transactionExpectations); - $this->fillMetadataExpectations($transactionExpectations); - $this->fillMetricSetExpectations($transactionExpectations); - $this->fillTraceExpectations($transactionExpectations); + self::addErrorExpectations($transactionExpectations); + self::addMetadataExpectations($appCodeInvocation, $transactionExpectations); + self::addMetricSetExpectations($transactionExpectations); + self::addTraceExpectations($appCodeInvocation, $transactionExpectations); } - private function deriveIsSampledExpectation(): ?bool + private static function deriveIsSampledExpectation(AppCodeInvocation $appCodeInvocation): ?bool { /** @var ?bool $sampleRate */ $sampleRate = null; - foreach ($this->appCodeInvocation->appCodeHostsParams as $appCodeHostParams) { + foreach ($appCodeInvocation->appCodeHostsParams as $appCodeHostParams) { $currentSampleRate = self::deriveIsSampledExpectationForAppCodeHost($appCodeHostParams); if ($sampleRate === null) { $sampleRate = $currentSampleRate; @@ -89,28 +102,30 @@ private static function deriveIsSampledExpectationForAppCodeHost(AppCodeHostPara return $sampleRate === 0.0 ? false : ($sampleRate === 1.0 ? true : null); } - private function fillErrorExpectations(TransactionExpectations $transactionExpectations): void + private function addErrorExpectations(TransactionExpectations $transactionExpectations): void { - $this->error = new ErrorExpectations(); + $errorExpectations = new ErrorExpectations(); TestCaseBase::assertGreaterThanZero( - self::setCommonProperties(/* src */ $transactionExpectations, /* dst */ $this->error) + self::setCommonProperties(/* src */ $transactionExpectations, /* dst */ $errorExpectations) ); + $this->errors[] = $errorExpectations; } - private function fillMetadataExpectations(TransactionExpectations $transactionExpectations): void - { - $this->agentEphemeralIdToMetadata = []; - foreach ($this->appCodeInvocation->appCodeHostsParams as $appCodeHostParams) { + private function addMetadataExpectations( + AppCodeInvocation $appCodeInvocation, + TransactionExpectations $transactionExpectations + ): void { + foreach ($appCodeInvocation->appCodeHostsParams as $appCodeHostParams) { $metadataExpectations - = self::buildMetadataExpectationsForHost($transactionExpectations, $appCodeHostParams); + = self::buildMetadataExpectationsForHost($appCodeHostParams, $transactionExpectations); $metadataExpectations->agentEphemeralId->setValue($appCodeHostParams->spawnedProcessInternalId); $this->agentEphemeralIdToMetadata[$appCodeHostParams->spawnedProcessInternalId] = $metadataExpectations; } } private static function buildMetadataExpectationsForHost( - TransactionExpectations $transactionExpectations, - AppCodeHostParams $appCodeHostParams + AppCodeHostParams $appCodeHostParams, + TransactionExpectations $transactionExpectations ): MetadataExpectations { $metadata = new MetadataExpectations(); TestCaseBase::assertGreaterThanZero( @@ -135,32 +150,36 @@ private static function buildMetadataExpectationsForHost( return $metadata; } - private function fillMetricSetExpectations(TransactionExpectations $transactionExpectations): void + private function addMetricSetExpectations(TransactionExpectations $transactionExpectations): void { - $this->metricSet = new MetricSetExpectations(); + $metricSetExpectations = new MetricSetExpectations(); TestCaseBase::assertGreaterThanZero( - self::setCommonProperties(/* src */ $transactionExpectations, /* dst */ $this->metricSet) + self::setCommonProperties(/* src */ $transactionExpectations, /* dst */ $metricSetExpectations) ); + $this->metricSets[] = $metricSetExpectations; } - private function fillTraceExpectations(TransactionExpectations $transactionExpectations): void - { - $this->trace = new TraceExpectations(); - $this->trace->transaction = $transactionExpectations; - self::setCommonProperties(/* src */ $transactionExpectations, /* dst */ $this->trace->span); - $this->trace->span->timestampAfter = $this->appCodeInvocation->timestampAfter; + private function addTraceExpectations( + AppCodeInvocation $appCodeInvocation, + TransactionExpectations $transactionExpectations + ): void { + $traceExpectations = new TraceExpectations(); + $traceExpectations->transaction = $transactionExpectations; + self::setCommonProperties(/* src */ $transactionExpectations, /* dst */ $traceExpectations->span); + $traceExpectations->span->timestampAfter = $appCodeInvocation->timestampAfter; - $appCodeRequestParams = $this->appCodeInvocation->appCodeRequestParams; - $this->trace->shouldVerifyRootTransaction = $appCodeRequestParams->shouldVerifyRootTransaction; - $this->trace->rootTransactionName = $appCodeRequestParams->expectedTransactionName->getValue(); - $this->trace->rootTransactionType = $appCodeRequestParams->expectedTransactionType->getValue(); + $appCodeRequestParams = $appCodeInvocation->appCodeRequestParams; + $traceExpectations->shouldVerifyRootTransaction = $appCodeRequestParams->shouldVerifyRootTransaction; + $traceExpectations->rootTransactionName = $appCodeRequestParams->expectedTransactionName->getValue(); + $traceExpectations->rootTransactionType = $appCodeRequestParams->expectedTransactionType->getValue(); if ($appCodeRequestParams instanceof HttpAppCodeRequestParams) { - $this->trace->isRootTransactionHttp = true; - $this->trace->rootTransactionHttpRequestMethod = $appCodeRequestParams->httpRequestMethod; - $this->trace->rootTransactionUrlParts = $appCodeRequestParams->urlParts; + $traceExpectations->isRootTransactionHttp = true; + $traceExpectations->rootTransactionHttpRequestMethod = $appCodeRequestParams->httpRequestMethod; + $traceExpectations->rootTransactionUrlParts = $appCodeRequestParams->urlParts; } else { - $this->trace->isRootTransactionHttp = false; + $traceExpectations->isRootTransactionHttp = false; } + $this->traces[] = $traceExpectations; } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawValidator.php b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawValidator.php index cb31e93d0..22252dde9 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawValidator.php +++ b/tests/ElasticApmTests/ComponentTests/Util/DataFromAgentPlusRawValidator.php @@ -59,7 +59,7 @@ private function validateImpl(): void { DataFromAgentValidator::validate($this->actual, $this->expectations); - foreach ($this->actual->intakeApiRequests as $intakeApiRequest) { + foreach ($this->actual->getAllIntakeApiRequests() as $intakeApiRequest) { $this->validateIntakeApiRequest($intakeApiRequest); } } @@ -74,9 +74,11 @@ private function validateIntakeApiRequest(IntakeApiRequest $intakeApiRequest): v private function findAppCodeHostsParamsBySpawnedProcessInternalId( string $spawnedProcessInternalId ): AppCodeHostParams { - foreach ($this->expectations->appCodeInvocation->appCodeHostsParams as $appCodeHostParams) { - if ($appCodeHostParams->spawnedProcessInternalId === $spawnedProcessInternalId) { - return $appCodeHostParams; + foreach ($this->expectations->appCodeInvocations as $appCodeInvocation) { + foreach ($appCodeInvocation->appCodeHostsParams as $appCodeHostParams) { + if ($appCodeHostParams->spawnedProcessInternalId === $spawnedProcessInternalId) { + return $appCodeHostParams; + } } } TestCase::fail( diff --git a/tests/ElasticApmTests/ComponentTests/Util/GlobalTestInfra.php b/tests/ElasticApmTests/ComponentTests/Util/GlobalTestInfra.php new file mode 100644 index 000000000..82c3cd049 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/GlobalTestInfra.php @@ -0,0 +1,118 @@ +resourcesCleaner = $this->startResourcesCleaner(); + $this->mockApmServer = $this->startMockApmServer($this->resourcesCleaner); + } + + public function onTestStart(): void + { + $this->cleanTestScoped(); + } + + public function onTestEnd(): void + { + $this->cleanTestScoped(); + } + + private function cleanTestScoped(): void + { + $this->mockApmServer->cleanTestScoped(); + $this->resourcesCleaner->cleanTestScoped(); + } + + public function getResourcesCleaner(): ResourcesCleanerHandle + { + return $this->resourcesCleaner; + } + + public function getMockApmServer(): MockApmServerHandle + { + return $this->mockApmServer; + } + + /** + * @return int[] + */ + public function getPortsInUse(): array + { + return $this->portsInUse; + } + + /** + * @param int[] $ports + * + * @return void + */ + private function addPortsInUse(array $ports): void + { + foreach ($ports as $port) { + Assert::assertNotContains($port, $this->portsInUse); + $this->portsInUse[] = $port; + } + } + + private function startResourcesCleaner(): ResourcesCleanerHandle + { + $httpServerHandle = TestInfraHttpServerStarter::startTestInfraHttpServer( + ClassNameUtil::fqToShort(ResourcesCleaner::class) /* <- dbgProcessName */, + 'runResourcesCleaner.php' /* <- runScriptName */, + $this->portsInUse, + 1 /* <- portsToAllocateCount */, + null /* <- resourcesCleaner */ + ); + $this->addPortsInUse($httpServerHandle->getPorts()); + return new ResourcesCleanerHandle($httpServerHandle); + } + + private function startMockApmServer(ResourcesCleanerHandle $resourcesCleaner): MockApmServerHandle + { + $httpServerHandle = TestInfraHttpServerStarter::startTestInfraHttpServer( + ClassNameUtil::fqToShort(MockApmServer::class) /* <- dbgProcessName */, + 'runMockApmServer.php' /* <- runScriptName */, + $this->portsInUse, + 2 /* <- portsToAllocateCount */, + $resourcesCleaner + ); + $this->addPortsInUse($httpServerHandle->getPorts()); + return new MockApmServerHandle($httpServerHandle); + } +} diff --git a/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeHostHandle.php b/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeHostHandle.php index b88d72891..068bbf597 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeHostHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeHostHandle.php @@ -56,9 +56,9 @@ public function __construct( )->addContext('this', $this); } - public function getPort(): int + public function getHttpServerHandle(): HttpServerHandle { - return $this->httpServerHandle->getPort(); + return $this->httpServerHandle; } public function buildDataPerRequest(AppCodeTarget $appCodeTarget): TestInfraDataPerRequest diff --git a/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeRequestParams.php b/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeRequestParams.php index 81fa8511a..b0a6aeafb 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeRequestParams.php +++ b/tests/ElasticApmTests/ComponentTests/Util/HttpAppCodeRequestParams.php @@ -43,7 +43,7 @@ public function __construct(HttpServerHandle $httpServerHandle, AppCodeTarget $a $this->urlParts = new UrlParts(); $this->urlParts->scheme('http') ->host(HttpServerHandle::DEFAULT_HOST) - ->port($httpServerHandle->getPort()) + ->port($httpServerHandle->getMainPort()) ->path('/'); } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/HttpServerHandle.php b/tests/ElasticApmTests/ComponentTests/Util/HttpServerHandle.php index a10e8f33e..2b849678d 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/HttpServerHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/HttpServerHandle.php @@ -24,11 +24,13 @@ namespace ElasticApmTests\ComponentTests\Util; use Elastic\Apm\Impl\Log\LoggableInterface; +use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Log\LoggableTrait; +use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\JsonUtil; use Elastic\Apm\Impl\Util\UrlParts; use GuzzleHttp\Exception\GuzzleException; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; class HttpServerHandle implements LoggableInterface @@ -48,19 +50,25 @@ class HttpServerHandle implements LoggableInterface /** @var string */ private $spawnedProcessInternalId; - /** @var int */ - private $port; + /** @var int[] */ + private $ports; + /** + * @param string $dbgServerDesc + * @param int $spawnedProcessOsId + * @param string $spawnedProcessInternalId + * @param int[] $ports + */ public function __construct( string $dbgServerDesc, int $spawnedProcessOsId, string $spawnedProcessInternalId, - int $port + array $ports ) { $this->dbgServerDesc = $dbgServerDesc; $this->spawnedProcessOsId = $spawnedProcessOsId; $this->spawnedProcessInternalId = $spawnedProcessInternalId; - $this->port = $port; + $this->ports = $ports; } public function getSpawnedProcessOsId(): int @@ -73,9 +81,18 @@ public function getSpawnedProcessInternalId(): string return $this->spawnedProcessInternalId; } - public function getPort(): int + /** + * @return int[] + */ + public function getPorts(): array + { + return $this->ports; + } + + public function getMainPort(): int { - return $this->port; + Assert::assertNotEmpty($this->ports); + return $this->ports[0]; } /** @@ -91,7 +108,7 @@ public function sendRequest(string $httpMethod, string $path, array $headers = [ { return HttpClientUtilForTests::sendRequest( $httpMethod, - (new UrlParts())->path($path)->port($this->getPort()), + (new UrlParts())->path($path)->port($this->getMainPort()), TestInfraDataPerRequest::withSpawnedProcessInternalId($this->spawnedProcessInternalId), $headers ); @@ -103,14 +120,14 @@ public function signalAndWaitForItToExit(): void HttpConstantsForTests::METHOD_POST, TestInfraHttpServerProcessBase::EXIT_URI_PATH ); - TestCase::assertSame(HttpConstantsForTests::STATUS_OK, $response->getStatusCode()); + Assert::assertSame(HttpConstantsForTests::STATUS_OK, $response->getStatusCode()); $hasExited = ProcessUtilForTests::waitForProcessToExit( $this->dbgServerDesc, $this->spawnedProcessOsId, 10 * 1000 * 1000 /* <- maxWaitTimeInMicroseconds - 10 seconds */ ); - TestCase::assertTrue($hasExited); + Assert::assertTrue($hasExited); } public function serialize(): string @@ -121,25 +138,42 @@ public function serialize(): string public static function deserialize(string $serialized): HttpServerHandle { $decodeJson = JsonUtil::decode($serialized, /* asAssocArray: */ true); - TestCase::assertIsArray($decodeJson); + Assert::assertIsArray($decodeJson); $getDecodedIntValue = function (string $propName) use ($decodeJson): int { - TestCase::assertArrayHasKey($propName, $decodeJson); - TestCase::assertIsInt($decodeJson[$propName]); + Assert::assertArrayHasKey($propName, $decodeJson); + Assert::assertIsInt($decodeJson[$propName]); return $decodeJson[$propName]; }; $getDecodedStringValue = function (string $propName) use ($decodeJson): string { - TestCase::assertArrayHasKey($propName, $decodeJson); - TestCase::assertIsString($decodeJson[$propName]); + Assert::assertArrayHasKey($propName, $decodeJson); + Assert::assertIsString($decodeJson[$propName]); return $decodeJson[$propName]; }; + /** + * @param string $propName + * + * @return int[] + */ + $getDecodedIntArrayValue = function (string $propName) use ($decodeJson): array { + Assert::assertArrayHasKey($propName, $decodeJson, LoggableToString::convert(['decodeJson' => $decodeJson])); + $propVal = $decodeJson[$propName]; + Assert::assertIsArray($propVal); + Assert::assertNotEmpty($propVal); + Assert::assertNotEmpty(ArrayUtil::isList($propVal)); + foreach ($propVal as $arrayElement) { + Assert::assertIsInt($arrayElement); + } + return $propVal; + }; + return new self( $getDecodedStringValue('dbgServerDesc'), $getDecodedIntValue('spawnedProcessOsId'), $getDecodedStringValue('spawnedProcessInternalId'), - $getDecodedIntValue('port') + $getDecodedIntArrayValue('ports') ); } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/HttpServerStarter.php b/tests/ElasticApmTests/ComponentTests/Util/HttpServerStarter.php index d3f093e57..e0117bd95 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/HttpServerStarter.php +++ b/tests/ElasticApmTests/ComponentTests/Util/HttpServerStarter.php @@ -28,10 +28,12 @@ use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\JsonUtil; +use Elastic\Apm\Impl\Util\RangeUtil; use Elastic\Apm\Impl\Util\UrlParts; use ElasticApmTests\Util\ArrayUtilForTests; use ElasticApmTests\Util\LogCategoryForTests; use ElasticApmTests\Util\RandomUtilForTests; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use RuntimeException; use Throwable; @@ -65,41 +67,45 @@ protected function __construct(string $dbgServerDesc) } /** - * @param int $port + * @param int[] $ports * * @return string */ - abstract protected function buildCommandLine(int $port): string; + abstract protected function buildCommandLine(array $ports): string; /** * @param string $spawnedProcessInternalId - * @param int $port + * @param int[] $ports * * @return array */ - abstract protected function buildEnvVars(string $spawnedProcessInternalId, int $port): array; + abstract protected function buildEnvVars(string $spawnedProcessInternalId, array $ports): array; /** * @param int[] $portsInUse + * @param int $portsToAllocateCount * * @return HttpServerHandle */ - protected function startHttpServer(array $portsInUse): HttpServerHandle + protected function startHttpServer(array $portsInUse, int $portsToAllocateCount = 1): HttpServerHandle { + Assert::assertGreaterThanOrEqual(1, $portsToAllocateCount); /** @var ?int $lastTriedPort */ $lastTriedPort = ArrayUtil::isEmpty($portsInUse) ? null : ArrayUtilForTests::getLastValue($portsInUse); for ($tryCount = 0; $tryCount < self::MAX_TRIES_TO_START_SERVER; ++$tryCount) { - $currentTryPort = self::findFreePortToListen($portsInUse, $lastTriedPort); - $lastTriedPort = $currentTryPort; + /** @var int[] $currentTryPorts */ + $currentTryPorts = []; + self::findFreePortsToListen($portsInUse, $portsToAllocateCount, $lastTriedPort, /* out */ $currentTryPorts); + Assert::assertSame($portsToAllocateCount, count($currentTryPorts)); $currentTrySpawnedProcessInternalId = InfraUtilForTests::generateSpawnedProcessInternalId(); - $cmdLine = $this->buildCommandLine($currentTryPort); - $envVars = $this->buildEnvVars($currentTrySpawnedProcessInternalId, $currentTryPort); + $cmdLine = $this->buildCommandLine($currentTryPorts); + $envVars = $this->buildEnvVars($currentTrySpawnedProcessInternalId, $currentTryPorts); $logger = $this->logger->inherit()->addAllContext( [ 'tryCount' => $tryCount, 'maxTries' => self::MAX_TRIES_TO_START_SERVER, - 'currentTryPort' => $currentTryPort, + 'currentTryPorts' => $currentTryPorts, 'currentTrySpawnedProcessInternalId' => $currentTrySpawnedProcessInternalId, 'cmdLine' => $cmdLine, 'envVars' => $envVars, @@ -115,7 +121,7 @@ protected function startHttpServer(array $portsInUse): HttpServerHandle if ( $this->isHttpServerRunning( $currentTrySpawnedProcessInternalId, - $currentTryPort, + $currentTryPorts[0], $logger, /* ref */ $pid ) @@ -126,7 +132,7 @@ protected function startHttpServer(array $portsInUse): HttpServerHandle $this->dbgServerDesc, $pid, $currentTrySpawnedProcessInternalId, - $currentTryPort + $currentTryPorts ); } @@ -137,6 +143,29 @@ protected function startHttpServer(array $portsInUse): HttpServerHandle throw new RuntimeException('Failed to start ' . $this->dbgServerDesc . ' HTTP server'); } + /** + * @param int[] $portsInUse + * @param ?int $lastTriedPort + * @param int $portsToFindCount + * @param int[] &$result + * + * @return void + */ + private static function findFreePortsToListen( + array $portsInUse, + int $portsToFindCount, + ?int $lastTriedPort, + array &$result + ): void { + $result = []; + $lastTriedPortLocal = $lastTriedPort; + foreach (RangeUtil::generateUpTo($portsToFindCount) as $ignored) { + $foundPort = self::findFreePortToListen($portsInUse, $lastTriedPortLocal); + $result[] = $foundPort; + $lastTriedPortLocal = $foundPort; + } + } + /** * @param int[] $portsInUse * @param ?int $lastTriedPort diff --git a/tests/ElasticApmTests/ComponentTests/Util/InfraUtilForTests.php b/tests/ElasticApmTests/ComponentTests/Util/InfraUtilForTests.php index 231c428ee..8aaf60766 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/InfraUtilForTests.php +++ b/tests/ElasticApmTests/ComponentTests/Util/InfraUtilForTests.php @@ -36,9 +36,16 @@ public static function generateSpawnedProcessInternalId(): string return IdGenerator::generateId(/* idLengthInBytes */ 16); } + /** + * @param string $targetSpawnedProcessInternalId + * @param int[] $targetServerPorts + * @param ?ResourcesCleanerHandle $resourcesCleaner + * + * @return TestInfraDataPerProcess + */ public static function buildTestInfraDataPerProcess( string $targetSpawnedProcessInternalId, - ?int $targetServerPort, + array $targetServerPorts, ?ResourcesCleanerHandle $resourcesCleaner ): TestInfraDataPerProcess { $result = new TestInfraDataPerProcess(); @@ -51,11 +58,11 @@ public static function buildTestInfraDataPerProcess( if ($resourcesCleaner !== null) { $result->resourcesCleanerSpawnedProcessInternalId = $resourcesCleaner->getSpawnedProcessInternalId(); - $result->resourcesCleanerPort = $resourcesCleaner->getPort(); + $result->resourcesCleanerPort = $resourcesCleaner->getMainPort(); } $result->thisSpawnedProcessInternalId = $targetSpawnedProcessInternalId; - $result->thisServerPort = $targetServerPort; + $result->thisServerPorts = $targetServerPorts; return $result; } @@ -63,7 +70,7 @@ public static function buildTestInfraDataPerProcess( /** * @param array $baseEnvVars * @param string $targetSpawnedProcessInternalId - * @param ?int $targetServerPort + * @param int[] $targetServerPorts * @param ?ResourcesCleanerHandle $resourcesCleaner * @param string $dbgProcessName * @@ -72,14 +79,17 @@ public static function buildTestInfraDataPerProcess( public static function addTestInfraDataPerProcessToEnvVars( array $baseEnvVars, string $targetSpawnedProcessInternalId, - ?int $targetServerPort, + array $targetServerPorts, ?ResourcesCleanerHandle $resourcesCleaner, string $dbgProcessName ): array { $dataPerProcessOptName = AllComponentTestsOptionsMetadata::DATA_PER_PROCESS_OPTION_NAME; $dataPerProcessEnvVarName = ConfigUtilForTests::testOptionNameToEnvVarName($dataPerProcessOptName); - $dataPerProcess - = self::buildTestInfraDataPerProcess($targetSpawnedProcessInternalId, $targetServerPort, $resourcesCleaner); + $dataPerProcess = self::buildTestInfraDataPerProcess( + $targetSpawnedProcessInternalId, + $targetServerPorts, + $resourcesCleaner + ); return $baseEnvVars + [ SpawnedProcessBase::DBG_PROCESS_NAME_ENV_VAR_NAME => $dbgProcessName, diff --git a/tests/ElasticApmTests/ComponentTests/Util/IntakeApiConnection.php b/tests/ElasticApmTests/ComponentTests/Util/IntakeApiConnection.php new file mode 100644 index 000000000..f7470c0be --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/IntakeApiConnection.php @@ -0,0 +1,57 @@ +requests = $requests; + } + + /** + * @return IntakeApiRequest[] + */ + public function getIntakeApiRequests(): array + { + return $this->requests; + } +} diff --git a/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequest.php b/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequest.php index c21da6eac..d620b70a3 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequest.php +++ b/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequest.php @@ -27,10 +27,12 @@ use Elastic\Apm\Impl\Log\LoggableTrait; use ElasticApmTests\Util\Deserialization\JsonDeserializableInterface; use ElasticApmTests\Util\Deserialization\JsonDeserializableTrait; +use ElasticApmTests\Util\Deserialization\JsonSerializableTrait; use JsonSerializable; final class IntakeApiRequest implements JsonSerializable, JsonDeserializableInterface, LoggableInterface { + use JsonSerializableTrait; use JsonDeserializableTrait; use LoggableTrait; diff --git a/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequestDeserializer.php b/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequestDeserializer.php index 57292a8bd..e8ae2bbc3 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequestDeserializer.php +++ b/tests/ElasticApmTests/ComponentTests/Util/IntakeApiRequestDeserializer.php @@ -57,7 +57,7 @@ public static function deserialize(IntakeApiRequest $intakeApiRequest): DataFrom private function __construct(IntakeApiRequest $intakeApiRequest) { $this->intakeApiRequest = $intakeApiRequest; - $this->result = new DataFromAgentPlusRaw(); + $this->result = new DataFromAgent(); $this->logger = AmbientContextForTests::loggerFactory()->loggerForClass( LogCategoryForTests::TEST_UTIL, diff --git a/tests/ElasticApmTests/ComponentTests/Util/MockApmServer.php b/tests/ElasticApmTests/ComponentTests/Util/MockApmServer.php index 1d44324dc..aa43daf7c 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/MockApmServer.php +++ b/tests/ElasticApmTests/ComponentTests/Util/MockApmServer.php @@ -29,11 +29,12 @@ use Elastic\Apm\Impl\Util\NumericUtil; use Elastic\Apm\Impl\Util\TextUtil; use ElasticApmTests\Util\LogCategoryForTests; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Promise\Promise; +use React\Socket\ConnectionInterface; final class MockApmServer extends TestInfraHttpServerProcessBase { @@ -41,14 +42,14 @@ final class MockApmServer extends TestInfraHttpServerProcessBase private const INTAKE_API_URI = '/intake/v2/events'; public const GET_INTAKE_API_REQUESTS = 'get_intake_api_requests'; public const FROM_INDEX_HEADER_NAME = RequestHeadersRawSnapshotSource::HEADER_NAMES_PREFIX . 'FROM_INDEX'; - public const INTAKE_API_REQUESTS_JSON_KEY = 'intake_api_requests_received_from_agent'; + public const RAW_DATA_FROM_AGENT_RECEIVER_EVENTS_JSON_KEY = 'raw_data_from_agent_receiver_events'; public const DATA_FROM_AGENT_MAX_WAIT_TIME_SECONDS = 10; - /** @var int */ - public static $pendingDataRequestNextId = 1; + /** @var RawDataFromAgentReceiverEvent[] */ + private $receiverEvents; - /** @var IntakeApiRequest[] */ - private $receivedIntakeApiRequests = []; + /** @var int */ + public $pendingDataRequestNextId; /** @var Map */ private $pendingDataRequests; @@ -61,6 +62,7 @@ public function __construct() parent::__construct(); $this->pendingDataRequests = new Map(); + $this->cleanTestScoped(); $this->logger = AmbientContextForTests::loggerFactory()->loggerForClass( LogCategoryForTests::TEST_UTIL, @@ -70,6 +72,40 @@ public function __construct() )->addContext('this', $this); } + /** + * @return int + */ + protected function expectedPortsCount(): int + { + return 2; + } + + /** @inheritDoc */ + protected function onNewConnection(int $socketIndex, ConnectionInterface $connection): void + { + parent::onNewConnection($socketIndex, $connection); + Assert::assertCount(2, $this->serverSockets); + Assert::assertLessThan(count($this->serverSockets), $socketIndex); + + // $socketIndex 0 is used for test infrastructure communication + // $socketIndex 1 is used for APM Agent <-> Server communication + if ($socketIndex == 1) { + $this->addReceiverEvent(new RawDataFromAgentReceiverEventConnectionStarted()); + } + } + + private function addReceiverEvent(RawDataFromAgentReceiverEvent $event): void + { + Assert::assertNotNull($this->reactLoop); + $this->receiverEvents[] = $event; + + foreach ($this->pendingDataRequests as $pendingDataRequest) { + $this->reactLoop->cancelTimer($pendingDataRequest->timer); + ($pendingDataRequest->resolveCallback)($this->fulfillDataRequest($pendingDataRequest->fromIndex)); + } + $this->pendingDataRequests->clear(); + } + /** @inheritDoc */ protected function processRequest(ServerRequestInterface $request) { @@ -81,6 +117,11 @@ protected function processRequest(ServerRequestInterface $request) return $this->processMockApiRequest($request); } + if ($request->getUri()->getPath() === TestInfraHttpServerProcessBase::CLEAN_TEST_SCOPED_URI_PATH) { + $this->cleanTestScoped(); + return new Response(/* status: */ 200); + } + return null; } @@ -92,11 +133,11 @@ protected function shouldRequestHaveSpawnedProcessInternalId(ServerRequestInterf private function processIntakeApiRequest(ServerRequestInterface $request): ResponseInterface { - TestCase::assertNotNull($this->reactLoop); + Assert::assertNotNull($this->reactLoop); if ($request->getBody()->getSize() === 0) { return $this->buildIntakeApiErrorResponse( - 400 /* status */, + HttpConstantsForTests::STATUS_BAD_REQUEST /* status */, 'Intake API request should not have empty body' ); } @@ -109,13 +150,7 @@ private function processIntakeApiRequest(ServerRequestInterface $request): Respo ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Received request for Intake API', ['newRequest' => $newRequest]); - $this->receivedIntakeApiRequests[] = $newRequest; - - foreach ($this->pendingDataRequests as $pendingDataRequest) { - $this->reactLoop->cancelTimer($pendingDataRequest->timer); - ($pendingDataRequest->resolveCallback)($this->fulfillDataRequest($pendingDataRequest->fromIndex)); - } - $this->pendingDataRequests->clear(); + $this->addReceiverEvent(new RawDataFromAgentReceiverEventRequest($newRequest)); return new Response(/* status: */ 202); } @@ -133,7 +168,10 @@ private function processMockApiRequest(ServerRequestInterface $request) return $this->getIntakeApiRequests($request); } - return $this->buildErrorResponse(400 /* status */, 'Unknown Mock API command `' . $command . '\''); + return $this->buildErrorResponse( + HttpConstantsForTests::STATUS_BAD_REQUEST, + 'Unknown Mock API command `' . $command . '\'' + ); } /** @@ -144,11 +182,11 @@ private function processMockApiRequest(ServerRequestInterface $request) private function getIntakeApiRequests(ServerRequestInterface $request) { $fromIndex = intval(self::getRequiredRequestHeader($request, self::FROM_INDEX_HEADER_NAME)); - if (!NumericUtil::isInClosedInterval(0, $fromIndex, count($this->receivedIntakeApiRequests))) { + if (!NumericUtil::isInClosedInterval(0, $fromIndex, count($this->receiverEvents))) { return $this->buildErrorResponse( - 400 /* status */, - 'Invalid `' . self::FROM_INDEX_HEADER_NAME . '\' HTTP request header value: $fromIndex' - . ' (should be in range[0, ' . count($this->receivedIntakeApiRequests) . '])' + HttpConstantsForTests::STATUS_BAD_REQUEST /* status */, + 'Invalid `' . self::FROM_INDEX_HEADER_NAME . '\' HTTP request header value: ' . $fromIndex + . ' (should be in range[0, ' . count($this->receiverEvents) . '])' ); } @@ -158,8 +196,8 @@ private function getIntakeApiRequests(ServerRequestInterface $request) return new Promise( function ($resolve) use ($fromIndex) { - $pendingDataRequestId = self::$pendingDataRequestNextId++; - TestCase::assertNotNull($this->reactLoop); + $pendingDataRequestId = $this->pendingDataRequestNextId++; + Assert::assertNotNull($this->reactLoop); $timer = $this->reactLoop->addTimer( self::DATA_FROM_AGENT_MAX_WAIT_TIME_SECONDS, function () use ($pendingDataRequestId) { @@ -176,13 +214,13 @@ function () use ($pendingDataRequestId) { private function hasNewDataFromAgentRequest(int $fromIndex): bool { - return count($this->receivedIntakeApiRequests) > $fromIndex; + return count($this->receiverEvents) > $fromIndex; } private function fulfillDataRequest(int $fromIndex): ResponseInterface { $newData = $this->hasNewDataFromAgentRequest($fromIndex) - ? array_slice($this->receivedIntakeApiRequests, $fromIndex) + ? array_slice($this->receiverEvents, $fromIndex) : []; ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) @@ -193,7 +231,7 @@ private function fulfillDataRequest(int $fromIndex): ResponseInterface // headers: ['Content-Type' => 'application/json'], // body: - JsonUtil::encode([self::INTAKE_API_REQUESTS_JSON_KEY => $newData], /* prettyPrint: */ true) + JsonUtil::encode([self::RAW_DATA_FROM_AGENT_RECEIVER_EVENTS_JSON_KEY => $newData], /* prettyPrint: */ true) ); } @@ -238,10 +276,17 @@ protected function buildIntakeApiErrorResponse(int $status, string $message): Re ); } + private function cleanTestScoped(): void + { + $this->receiverEvents = []; + $this->pendingDataRequestNextId = 1; + $this->pendingDataRequests->clear(); + } + /** @inheritDoc */ protected function exit(): void { - TestCase::assertNotNull($this->reactLoop); + Assert::assertNotNull($this->reactLoop); foreach ($this->pendingDataRequests as $pendingDataRequest) { $this->reactLoop->cancelTimer($pendingDataRequest->timer); diff --git a/tests/ElasticApmTests/ComponentTests/Util/MockApmServerHandle.php b/tests/ElasticApmTests/ComponentTests/Util/MockApmServerHandle.php index 7b7dc80b9..cad861a50 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/MockApmServerHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/MockApmServerHandle.php @@ -23,11 +23,13 @@ namespace ElasticApmTests\ComponentTests\Util; +use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\JsonUtil; use ElasticApmTests\Util\LogCategoryForTests; +use PHPUnit\Framework\Assert; use RuntimeException; final class MockApmServerHandle extends HttpServerHandle @@ -44,7 +46,7 @@ public function __construct(HttpServerHandle $httpSpawnedProcessHandle) ClassNameUtil::fqToShort(MockApmServer::class) /* <- dbgServerDesc */, $httpSpawnedProcessHandle->getSpawnedProcessOsId(), $httpSpawnedProcessHandle->getSpawnedProcessInternalId(), - $httpSpawnedProcessHandle->getPort() + $httpSpawnedProcessHandle->getPorts() ); $this->logger = AmbientContextForTests::loggerFactory()->loggerForClass( @@ -55,8 +57,14 @@ public function __construct(HttpServerHandle $httpSpawnedProcessHandle) )->addContext('this', $this); } + public function getPortForAgent(): int + { + Assert::assertCount(2, $this->getPorts()); + return $this->getPorts()[1]; + } + /** - * @return IntakeApiRequest[] + * @return RawDataFromAgentReceiverEvent[] */ public function fetchNewData(): array { @@ -69,33 +77,52 @@ public function fetchNewData(): array [MockApmServer::FROM_INDEX_HEADER_NAME => strval($this->nextIntakeApiRequestIndexToFetch)] ); + $responseBody = $response->getBody()->getContents(); if ($response->getStatusCode() !== HttpConstantsForTests::STATUS_OK) { - throw new RuntimeException('Received unexpected status code'); + throw new RuntimeException( + 'Received unexpected status code; ' . LoggableToString::convert( + [ + 'expected' => HttpConstantsForTests::STATUS_OK, + 'actual' => $response->getStatusCode(), + 'body' => $responseBody, + ] + ) + ); } /** @var array $decodedBody */ - $decodedBody = JsonUtil::decode($response->getBody()->getContents(), /* asAssocArray */ true); - - $requestsJson = $decodedBody[MockApmServer::INTAKE_API_REQUESTS_JSON_KEY]; - /** @var array> $requestsJson */ - $newIntakeApiRequests = []; - foreach ($requestsJson as $requestJson) { - $newIntakeApiRequest = new IntakeApiRequest(); - $newIntakeApiRequest->deserializeFromDecodedJson($requestJson); - $newIntakeApiRequests[] = $newIntakeApiRequest; + $decodedBody = JsonUtil::decode($responseBody, /* asAssocArray */ true); + + $receiverEventsJson = $decodedBody[MockApmServer::RAW_DATA_FROM_AGENT_RECEIVER_EVENTS_JSON_KEY]; + /** @var array> $receiverEventsJson */ + $newReceiverEvents = []; + foreach ($receiverEventsJson as $receiverEventJson) { + $newReceiverEvent = RawDataFromAgentReceiverEvent::deserializeFromDecodedJson($receiverEventJson); + $newReceiverEvents[] = $newReceiverEvent; } - if (ArrayUtil::isEmpty($newIntakeApiRequests)) { + if (ArrayUtil::isEmpty($newReceiverEvents)) { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log('Fetched NO new intake API requests received from agent'); + && $loggerProxy->log('Fetched NO new data from agent receiver events'); } else { - $this->nextIntakeApiRequestIndexToFetch += count($newIntakeApiRequests); + $this->nextIntakeApiRequestIndexToFetch += count($newReceiverEvents); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( - 'Fetched new intake API requests received from agent', - ['count($newIntakeApiRequests)' => count($newIntakeApiRequests)] + 'Fetched new data from agent receiver events', + ['count(newReceiverEvents)' => count($newReceiverEvents)] ); } - return $newIntakeApiRequests; + return $newReceiverEvents; + } + + public function cleanTestScoped(): void + { + $this->nextIntakeApiRequestIndexToFetch = 0; + + $response = $this->sendRequest( + HttpConstantsForTests::METHOD_POST, + TestInfraHttpServerProcessBase::CLEAN_TEST_SCOPED_URI_PATH + ); + Assert::assertSame(HttpConstantsForTests::STATUS_OK, $response->getStatusCode()); } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgent.php b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgent.php new file mode 100644 index 000000000..297e91564 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgent.php @@ -0,0 +1,73 @@ +intakeApiConnections = $intakeApiConnections; + } + + /** + * @return IntakeApiConnection[] + */ + public function getIntakeApiConnections(): array + { + return $this->intakeApiConnections; + } + + /** + * @return IntakeApiRequest[] + */ + public function getAllIntakeApiRequests(): array + { + if ($this->allIntakeApiRequests === null) { + $this->allIntakeApiRequests = []; + foreach ($this->intakeApiConnections as $intakeApiConnection) { + ArrayUtilForTests::append( + $intakeApiConnection->getIntakeApiRequests() /* <- from */, + $this->allIntakeApiRequests /* <- to, ref */ + ); + } + } + return $this->allIntakeApiRequests; + } + + public function getTimeAllDataReceivedAtApmServer(): float + { + return ArrayUtilForTests::getLastValue($this->getAllIntakeApiRequests())->timeReceivedAtApmServer; + } +} diff --git a/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEvent.php b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEvent.php new file mode 100644 index 000000000..cbdbab0f9 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEvent.php @@ -0,0 +1,66 @@ +shortClassName = ClassNameUtil::fqToShort(get_called_class()); + } + + abstract public function visit(RawDataFromAgentReceiverEventVisitorInterface $visitor): void; + + /** + * @param array $decodedJson + */ + public static function deserializeFromDecodedJson(array $decodedJson): self + { + Assert::assertArrayHasKey('shortClassName', $decodedJson); + $shortClassName = $decodedJson['shortClassName']; + + if ($shortClassName === ClassNameUtil::fqToShort(RawDataFromAgentReceiverEventConnectionStarted::class)) { + return RawDataFromAgentReceiverEventConnectionStarted::leafDeserializeFromDecodedJson($decodedJson); + } + if ($shortClassName === ClassNameUtil::fqToShort(RawDataFromAgentReceiverEventRequest::class)) { + return RawDataFromAgentReceiverEventRequest::leafDeserializeFromDecodedJson($decodedJson); + } + + Assert::fail(LoggableToString::convert(['shortClassName' => $shortClassName, 'decodedJson' => $decodedJson])); + } +} diff --git a/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventConnectionStarted.php b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventConnectionStarted.php new file mode 100644 index 000000000..ce32d1fb1 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventConnectionStarted.php @@ -0,0 +1,44 @@ + $decodedJson + * + * @return self + */ + public static function leafDeserializeFromDecodedJson(array $decodedJson): self + { + return new self(); + } + + public function visit(RawDataFromAgentReceiverEventVisitorInterface $visitor): void + { + $visitor->visitConnectionStarted($this); + } +} diff --git a/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventRequest.php b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventRequest.php new file mode 100644 index 000000000..dcefb4799 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventRequest.php @@ -0,0 +1,59 @@ +request = $request; + } + + /** + * @param array $decodedJson + * + * @return self + */ + public static function leafDeserializeFromDecodedJson(array $decodedJson): self + { + $intakeApiRequest = new IntakeApiRequest(); + Assert::assertArrayHasKey('request', $decodedJson); + $decodedJsonRequestSubPart = $decodedJson['request']; + Assert::assertIsArray($decodedJsonRequestSubPart); + $intakeApiRequest->deserializeFromDecodedJson($decodedJsonRequestSubPart); + return new self($intakeApiRequest); + } + + public function visit(RawDataFromAgentReceiverEventVisitorInterface $visitor): void + { + $visitor->visitRequest($this); + } +} diff --git a/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventVisitorInterface.php b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventVisitorInterface.php new file mode 100644 index 000000000..6bea7d01c --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/Util/RawDataFromAgentReceiverEventVisitorInterface.php @@ -0,0 +1,30 @@ + */ - private $filesToDeletePaths; + private $globalFilesToDeletePaths; + + /** @var Set */ + private $testScopedFilesToDeletePaths; + + /** @var Set */ + private $globalProcessesToTerminateIds; /** @var Set */ - private $processesToTerminateIds; + private $testScopedProcessesToTerminateIds; /** @var ?TimerInterface */ private $parentProcessTrackingTimer = null; @@ -56,8 +65,11 @@ public function __construct() { parent::__construct(); - $this->filesToDeletePaths = new Set(); - $this->processesToTerminateIds = new Set(); + $this->globalFilesToDeletePaths = new Set(); + $this->testScopedFilesToDeletePaths = new Set(); + + $this->globalProcessesToTerminateIds = new Set(); + $this->testScopedProcessesToTerminateIds = new Set(); $this->logger = AmbientContextForTests::loggerFactory()->loggerForClass( LogCategoryForTests::TEST_UTIL, @@ -80,6 +92,8 @@ protected function processConfig(): void /** @inheritDoc */ protected function beforeLoopRun(): void { + parent::beforeLoopRun(); + TestCase::assertNotNull($this->reactLoop); $this->parentProcessTrackingTimer = $this->reactLoop->addPeriodicTimer( 1 /* interval in seconds */, @@ -97,8 +111,8 @@ function () { /** @inheritDoc */ protected function exit(): void { - $this->cleanSpawnedProcesses(); - $this->cleanFiles(); + $this->cleanSpawnedProcesses(/* isTestScopedOnly */ false); + $this->cleanFiles(/* isTestScopedOnly */ false); TestCase::assertNotNull($this->reactLoop); TestCase::assertNotNull($this->parentProcessTrackingTimer); @@ -107,15 +121,35 @@ protected function exit(): void parent::exit(); } - private function cleanSpawnedProcesses(): void + private function cleanSpawnedProcesses(bool $isTestScopedOnly): void + { + $this->cleanSpawnedProcessesFrom(/* dbgSetDesc */ 'test scoped', $this->testScopedProcessesToTerminateIds); + if (!$isTestScopedOnly) { + $this->cleanSpawnedProcessesFrom(/* dbgSetDesc */ 'global', $this->globalProcessesToTerminateIds); + } + } + + private function cleanTestScoped(): void + { + $this->cleanSpawnedProcesses(/* isTestScopedOnly */ true); + $this->cleanFiles(/* isTestScopedOnly */ true); + } + + /** + * @param string $dbgSetDesc + * @param Set $processesToTerminateIds + * + * @return void + */ + private function cleanSpawnedProcessesFrom(string $dbgSetDesc, Set $processesToTerminateIds): void { ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( - 'Terminating spawned processes...', - ['processesToTerminateIds count' => $this->processesToTerminateIds->count()] + 'Terminating spawned processes ()...', + ['dbgSetDesc' => $dbgSetDesc, 'processesToTerminateIds count' => $processesToTerminateIds->count()] ); - foreach ($this->processesToTerminateIds as $spawnedProcessesId) { + foreach ($processesToTerminateIds as $spawnedProcessesId) { if (!ProcessUtilForTests::doesProcessExist($spawnedProcessesId)) { ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( @@ -124,24 +158,49 @@ private function cleanSpawnedProcesses(): void ); continue; } - $termCmdExitCode = ProcessUtilForTests::terminateProcess($spawnedProcessesId); + $hasExitedNormally = ProcessUtilForTests::terminateProcess($spawnedProcessesId); + $hasExited = ProcessUtilForTests::waitForProcessToExit( + 'Spawn process' /* <- dbgProcessDesc */, + $spawnedProcessesId, + 10 * 1000 * 1000 /* <- maxWaitTimeInMicroseconds - 10 seconds */ + ); ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Issued command to terminate spawned process', - ['spawnedProcessesId' => $spawnedProcessesId, 'termCmdExitCode' => $termCmdExitCode] + [ + 'spawnedProcessesId' => $spawnedProcessesId, + 'hasExited' => $hasExited, + 'hasExitedNormally' => $hasExitedNormally + ] ); } + + $processesToTerminateIds->clear(); + } + + private function cleanFiles(bool $isTestScopedOnly): void + { + $this->cleanFilesFrom(/* dbgSetDesc */ 'test scoped', $this->testScopedFilesToDeletePaths); + if (!$isTestScopedOnly) { + $this->cleanFilesFrom(/* dbgSetDesc */ 'global', $this->globalFilesToDeletePaths); + } } - private function cleanFiles(): void + /** + * @param string $dbgSetDesc + * @param Set $filesToDeletePaths + * + * @return void + */ + private function cleanFilesFrom(string $dbgSetDesc, Set $filesToDeletePaths): void { ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Deleting files...', - ['filesToDeletePaths count' => $this->filesToDeletePaths->count()] + ['dbgSetDesc' => $dbgSetDesc, 'filesToDeletePaths count' => $filesToDeletePaths->count()] ); - foreach ($this->filesToDeletePaths as $fileToDeletePath) { + foreach ($filesToDeletePaths as $fileToDeletePath) { if (!file_exists($fileToDeletePath)) { ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( @@ -158,6 +217,8 @@ private function cleanFiles(): void ['fileToDeletePath' => $fileToDeletePath, 'unlinkRetVal' => $unlinkRetVal] ); } + + $filesToDeletePaths->clear(); } /** @inheritDoc */ @@ -165,38 +226,55 @@ protected function processRequest(ServerRequestInterface $request): ?ResponseInt { switch ($request->getUri()->getPath()) { case self::REGISTER_PROCESS_TO_TERMINATE_URI_PATH: - return $this->registerProcessToTerminate($request); + $this->registerProcessToTerminate($request); + break; case self::REGISTER_FILE_TO_DELETE_URI_PATH: - return $this->registerFileToDelete($request); + $this->registerFileToDelete($request); + break; + case self::CLEAN_TEST_SCOPED_URI_PATH: + $this->cleanTestScoped(); + break; default: return null; } + return self::buildDefaultResponse(); } - protected function registerProcessToTerminate(ServerRequestInterface $request): ResponseInterface + protected function registerProcessToTerminate(ServerRequestInterface $request): void { $pid = intval(self::getRequiredRequestHeader($request, self::PID_QUERY_HEADER_NAME)); - $this->processesToTerminateIds->add($pid); + $isTestScopedAsString = self::getRequiredRequestHeader($request, self::IS_TEST_SCOPED_QUERY_HEADER_NAME); + $isTestScoped = JsonUtil::decode($isTestScopedAsString, /* asAssocArray */ true); + $processesToTerminateIds + = $isTestScoped ? $this->testScopedProcessesToTerminateIds : $this->globalProcessesToTerminateIds; + $processesToTerminateIds->add($pid); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Successfully registered process to terminate', - ['pid' => $pid, 'processesToTerminateIds count' => $this->processesToTerminateIds->count()] + [ + 'pid' => $pid, + 'isTestScoped' => $isTestScoped, + 'processesToTerminateIds count' => $processesToTerminateIds->count(), + ] ); - - return self::buildDefaultResponse(); } - protected function registerFileToDelete(ServerRequestInterface $request): ResponseInterface + protected function registerFileToDelete(ServerRequestInterface $request): void { $path = self::getRequiredRequestHeader($request, self::PATH_QUERY_HEADER_NAME); - $this->filesToDeletePaths->add($path); + $isTestScopedAsString = self::getRequiredRequestHeader($request, self::IS_TEST_SCOPED_QUERY_HEADER_NAME); + $isTestScoped = JsonUtil::decode($isTestScopedAsString, /* asAssocArray */ true); + $filesToDeletePaths = $isTestScoped ? $this->testScopedFilesToDeletePaths : $this->globalFilesToDeletePaths; + $filesToDeletePaths->add($path); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Successfully registered file to delete', - ['path' => $path, 'filesToDeletePaths count' => $this->filesToDeletePaths->count()] + [ + 'path' => $path, + 'isTestScoped' => $isTestScoped, + 'filesToDeletePaths count' => $filesToDeletePaths->count(), + ] ); - - return self::buildDefaultResponse(); } protected function shouldRegisterThisProcessWithResourcesCleaner(): bool diff --git a/tests/ElasticApmTests/ComponentTests/Util/ResourcesCleanerHandle.php b/tests/ElasticApmTests/ComponentTests/Util/ResourcesCleanerHandle.php index e1e3d8634..bae354644 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ResourcesCleanerHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ResourcesCleanerHandle.php @@ -24,6 +24,7 @@ namespace ElasticApmTests\ComponentTests\Util; use Elastic\Apm\Impl\Util\ClassNameUtil; +use PHPUnit\Framework\Assert; final class ResourcesCleanerHandle extends HttpServerHandle { @@ -36,14 +37,23 @@ public function __construct(HttpServerHandle $httpSpawnedProcessHandle) ClassNameUtil::fqToShort(ResourcesCleaner::class) /* <- dbgServerDesc */, $httpSpawnedProcessHandle->getSpawnedProcessOsId(), $httpSpawnedProcessHandle->getSpawnedProcessInternalId(), - $httpSpawnedProcessHandle->getPort() + $httpSpawnedProcessHandle->getPorts() ); - $this->resourcesClient = new ResourcesClient($this->getSpawnedProcessInternalId(), $this->getPort()); + $this->resourcesClient = new ResourcesClient($this->getSpawnedProcessInternalId(), $this->getMainPort()); } public function getClient(): ResourcesClient { return $this->resourcesClient; } + + public function cleanTestScoped(): void + { + $response = $this->sendRequest( + HttpConstantsForTests::METHOD_POST, + TestInfraHttpServerProcessBase::CLEAN_TEST_SCOPED_URI_PATH + ); + Assert::assertSame(HttpConstantsForTests::STATUS_OK, $response->getStatusCode()); + } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/ResourcesClient.php b/tests/ElasticApmTests/ComponentTests/Util/ResourcesClient.php index 99b7cc25b..be0d61c52 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ResourcesClient.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ResourcesClient.php @@ -24,6 +24,7 @@ namespace ElasticApmTests\ComponentTests\Util; use Elastic\Apm\Impl\Log\Logger; +use Elastic\Apm\Impl\Util\BoolUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\ExceptionUtil; use Elastic\Apm\Impl\Util\UrlParts; @@ -54,7 +55,8 @@ public function __construct(string $resourcesCleanerSpawnedProcessInternalId, in $this->resourcesCleanerPort = $resourcesCleanerPort; } - private function registerFileToDelete(string $fullPath): void + /** @noinspection PhpSameParameterValueInspection */ + private function registerFileToDelete(string $fullPath, bool $isTestScoped): void { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( @@ -68,7 +70,11 @@ private function registerFileToDelete(string $fullPath): void ->path(ResourcesCleaner::REGISTER_FILE_TO_DELETE_URI_PATH) ->port($this->resourcesCleanerPort), TestInfraDataPerRequest::withSpawnedProcessInternalId($this->resourcesCleanerSpawnedProcessInternalId), - [ResourcesCleaner::PATH_QUERY_HEADER_NAME => $fullPath] /* <- headers */ + /* headers: */ + [ + ResourcesCleaner::PATH_QUERY_HEADER_NAME => $fullPath, + ResourcesCleaner::IS_TEST_SCOPED_QUERY_HEADER_NAME => BoolUtil::toString($isTestScoped), + ] ); if ($response->getStatusCode() !== HttpConstantsForTests::STATUS_OK) { throw new RuntimeException( @@ -102,7 +108,7 @@ public function createTempFile(string $type, bool $shouldBeDeletedOnTestExit = t } if ($shouldBeDeletedOnTestExit) { - $this->registerFileToDelete($tempFileFullPath); + $this->registerFileToDelete($tempFileFullPath, /* isTestScoped */ true); } return $tempFileFullPath; } diff --git a/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php b/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php index b1780878e..cbf2df839 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php @@ -32,6 +32,7 @@ use Elastic\Apm\Impl\Log\LoggableTrait; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Log\LoggingSubsystem; +use Elastic\Apm\Impl\Util\BoolUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\ExceptionUtil; use Elastic\Apm\Impl\Util\UrlParts; @@ -73,6 +74,10 @@ private static function buildLogger(): Logger __FILE__ ); } + + /** + * @return void + */ protected function processConfig(): void { self::getRequiredTestOption(AllComponentTestsOptionsMetadata::DATA_PER_PROCESS_OPTION_NAME); @@ -178,6 +183,11 @@ protected function shouldRegisterThisProcessWithResourcesCleaner(): bool return true; } + protected function isThisProcessTestScoped(): bool + { + return false; + } + protected function registerWithResourcesCleaner(): void { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) @@ -194,7 +204,11 @@ protected function registerWithResourcesCleaner(): void ->path(ResourcesCleaner::REGISTER_PROCESS_TO_TERMINATE_URI_PATH) ->port(AmbientContextForTests::testConfig()->dataPerProcess->resourcesCleanerPort), TestInfraDataPerRequest::withSpawnedProcessInternalId($resCleanerId), - [ResourcesCleaner::PID_QUERY_HEADER_NAME => strval(getmypid())] + [ + ResourcesCleaner::PID_QUERY_HEADER_NAME => strval(getmypid()), + ResourcesCleaner::IS_TEST_SCOPED_QUERY_HEADER_NAME + => BoolUtil::toString($this->isThisProcessTestScoped()), + ] ); if ($response->getStatusCode() !== HttpConstantsForTests::STATUS_OK) { throw new RuntimeException( diff --git a/tests/ElasticApmTests/ComponentTests/Util/SyslogClearerClient.php b/tests/ElasticApmTests/ComponentTests/Util/SyslogClearerClient.php index 8d182db65..1ac976d51 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/SyslogClearerClient.php +++ b/tests/ElasticApmTests/ComponentTests/Util/SyslogClearerClient.php @@ -58,6 +58,7 @@ public static function startInBackground(): void ClassNameUtil::fqToShort(SyslogClearer::class) /* <- dbgServerDesc */, 'runSyslogClearer.php' /* <- runScriptName */, [HttpServerStarter::PORTS_RANGE_END - 2] /* <- portsInUse */, + 1 /* <- portsToAllocateCount */, null /* <- resourcesCleaner */ ); diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestCaseHandle.php b/tests/ElasticApmTests/ComponentTests/Util/TestCaseHandle.php index 86f480b66..f87a3967f 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/TestCaseHandle.php +++ b/tests/ElasticApmTests/ComponentTests/Util/TestCaseHandle.php @@ -32,7 +32,6 @@ use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\TimeUtil; -use ElasticApmTests\Util\ArrayUtilForTests; use ElasticApmTests\Util\LogCategoryForTests; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; @@ -47,14 +46,14 @@ final class TestCaseHandle implements LoggableInterface public const SERIALIZED_EXPECTATIONS_KEY = 'serialized_expectations'; public const SERIALIZED_DATA_FROM_AGENT_KEY = 'serialized_data_from_agent'; - /** @var ?AppCodeInvocation */ - public $appCodeInvocation = null; - /** @var ResourcesCleanerHandle */ - protected $resourcesCleaner; + private $resourcesCleaner; /** @var MockApmServerHandle */ - protected $mockApmServer; + private $mockApmServer; + + /** @var AppCodeInvocation[] */ + public $appCodeInvocations = []; /** @var ?AppCodeHostHandle */ protected $mainAppCodeHost = null; @@ -66,7 +65,7 @@ final class TestCaseHandle implements LoggableInterface private $logger; /** @var int[] */ - private $portsInUse = []; + private $portsInUse; /** @var ?int */ private $escalatedLogLevelForProdCode; @@ -80,8 +79,12 @@ public function __construct(?int $escalatedLogLevelForProdCode) __FILE__ )->addContext('this', $this); - $this->resourcesCleaner = $this->startResourcesCleaner(); - $this->mockApmServer = $this->startMockApmServer($this->resourcesCleaner); + $globalTestInfra = ComponentTestsPhpUnitExtension::getGlobalTestInfra(); + $globalTestInfra->onTestStart(); + $this->resourcesCleaner = $globalTestInfra->getResourcesCleaner(); + $this->mockApmServer = $globalTestInfra->getMockApmServer(); + $this->portsInUse = $globalTestInfra->getPortsInUse(); + $this->escalatedLogLevelForProdCode = $escalatedLogLevelForProdCode; } @@ -152,7 +155,7 @@ public function waitForDataFromAgent( ExpectedEventCounts $expectedEventCounts, bool $shouldValidate = true ): DataFromAgentPlusRaw { - TestCase::assertNotNull($this->appCodeInvocation); + TestCase::assertNotEmpty($this->appCodeInvocations); $dataFromAgentAccumulator = new DataFromAgentPlusRawAccumulator(); $hasPassed = (new PollingCheck( __FUNCTION__ . ' passes', @@ -167,9 +170,9 @@ function () use ($expectedEventCounts, $dataFromAgentAccumulator) { 'The expected data from agent has not arrived.' . ' ' . LoggableToString::convert( [ - 'expected event counts' => $expectedEventCounts, - 'actual event counts' => $dataFromAgentAccumulator->dbgCounts(), - '$dataFromAgentAccumulator' => $dataFromAgentAccumulator, + 'expected event counts' => $expectedEventCounts, + 'actual event counts' => $dataFromAgentAccumulator->dbgCounts(), + 'dataFromAgentAccumulator' => $dataFromAgentAccumulator, ] ) ); @@ -177,13 +180,14 @@ function () use ($expectedEventCounts, $dataFromAgentAccumulator) { $dataFromAgent = $dataFromAgentAccumulator->getAccumulatedData(); if ($shouldValidate) { $expectations = new DataFromAgentPlusRawExpectations( - $this->appCodeInvocation, - ArrayUtilForTests::getLastValue($dataFromAgent->intakeApiRequests)->timeReceivedAtApmServer + $this->appCodeInvocations, + $dataFromAgent->getRaw()->getTimeAllDataReceivedAtApmServer() ); + $validatorClassName = ClassNameUtil::fqToShort(DataFromAgentPlusRawValidator::class); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( - 'Before DataFromAgentPlusRawValidator::validate: data that can be used for ' + 'Before ' . $validatorClassName . '::validate: data that can be used for ' . ClassNameUtil::fqToShort(DataFromAgentPlusRawValidatorDebugTest::class), [ self::SERIALIZED_EXPECTATIONS_KEY => serialize($expectations), @@ -192,7 +196,7 @@ function () use ($expectedEventCounts, $dataFromAgentAccumulator) { ); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( - 'Before DataFromAgentPlusRawValidator::validate', + 'Before ' . $validatorClassName . '::validate', ['expectations' => $expectations, 'dataFromAgent' => $dataFromAgent] ); DataFromAgentPlusRawValidator::validate($dataFromAgent, $expectations); @@ -207,12 +211,11 @@ private function setMandatoryOptions(AppCodeHostParams $params): void $escalatedLogLevelForProdCodeAsString = LogLevel::intToName($this->escalatedLogLevelForProdCode); $params->setAgentOption(OptionNames::LOG_LEVEL_SYSLOG, $escalatedLogLevelForProdCodeAsString); } - $params->setAgentOption(OptionNames::SERVER_URL, 'http://localhost:' . $this->mockApmServer->getPort()); + $params->setAgentOption(OptionNames::SERVER_URL, 'http://localhost:' . $this->mockApmServer->getPortForAgent()); } - public function setAppCodeInvocation(AppCodeInvocation $appCodeInvocation): void + public function addAppCodeInvocation(AppCodeInvocation $appCodeInvocation): void { - TestCase::assertNull($this->appCodeInvocation); $appCodeInvocation->appCodeHostsParams = []; if ($this->mainAppCodeHost !== null) { $appCodeInvocation->appCodeHostsParams[] = $this->mainAppCodeHost->appCodeHostParams; @@ -220,7 +223,7 @@ public function setAppCodeInvocation(AppCodeInvocation $appCodeInvocation): void if ($this->additionalHttpAppCodeHost !== null) { $appCodeInvocation->appCodeHostsParams[] = $this->additionalHttpAppCodeHost->appCodeHostParams; } - $this->appCodeInvocation = $appCodeInvocation; + $this->appCodeInvocations[] = $appCodeInvocation; } /** @@ -243,37 +246,20 @@ public function tearDown(): void ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Tearing down...'); - $this->resourcesCleaner->signalAndWaitForItToExit(); + ComponentTestsPhpUnitExtension::getGlobalTestInfra()->onTestEnd(); } - private function addPortInUse(int $port): void - { - TestCase::assertNotContains($port, $this->portsInUse); - $this->portsInUse[] = $port; - } - - private function startResourcesCleaner(): ResourcesCleanerHandle - { - $httpServerHandle = TestInfraHttpServerStarter::startTestInfraHttpServer( - ClassNameUtil::fqToShort(ResourcesCleaner::class) /* <- dbgProcessName */, - 'runResourcesCleaner.php' /* <- runScriptName */, - $this->portsInUse, - null /* <- resourcesCleaner */ - ); - $this->addPortInUse($httpServerHandle->getPort()); - return new ResourcesCleanerHandle($httpServerHandle); - } - - private function startMockApmServer(ResourcesCleanerHandle $resourcesCleaner): MockApmServerHandle + /** + * @param int[] $ports + * + * @return void + */ + private function addPortsInUse(array $ports): void { - $httpServerHandle = TestInfraHttpServerStarter::startTestInfraHttpServer( - ClassNameUtil::fqToShort(MockApmServer::class) /* <- dbgProcessName */, - 'runMockApmServer.php' /* <- runScriptName */, - $this->portsInUse, - $resourcesCleaner - ); - $this->addPortInUse($httpServerHandle->getPort()); - return new MockApmServerHandle($httpServerHandle); + foreach ($ports as $port) { + TestCase::assertNotContains($port, $this->portsInUse); + $this->portsInUse[] = $port; + } } private function startBuiltinHttpServerAppCodeHost( @@ -287,7 +273,7 @@ private function startBuiltinHttpServerAppCodeHost( $this->portsInUse, $dbgInstanceName ); - $this->addPortInUse($result->getPort()); + $this->addPortsInUse($result->getHttpServerHandle()->getPorts()); return $result; } @@ -322,8 +308,8 @@ private function pollForDataFromAgent( ExpectedEventCounts $expectedEventCounts, DataFromAgentPlusRawAccumulator $dataFromAgentAccumulator ): bool { - $newIntakeApiRequests = $this->mockApmServer->fetchNewData(); - $dataFromAgentAccumulator->addIntakeApiRequests($newIntakeApiRequests); + $newReceiverEvents = $this->mockApmServer->fetchNewData(); + $dataFromAgentAccumulator->addReceiverEvents($newReceiverEvents); return $dataFromAgentAccumulator->hasReachedEventCounts($expectedEventCounts); } diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestInfraData.php b/tests/ElasticApmTests/ComponentTests/Util/TestInfraData.php index c7f5c51e7..8db43e213 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/TestInfraData.php +++ b/tests/ElasticApmTests/ComponentTests/Util/TestInfraData.php @@ -27,10 +27,12 @@ use Elastic\Apm\Impl\Log\LoggableTrait; use ElasticApmTests\Util\Deserialization\JsonDeserializableInterface; use ElasticApmTests\Util\Deserialization\JsonDeserializableTrait; +use ElasticApmTests\Util\Deserialization\JsonSerializableTrait; use JsonSerializable; abstract class TestInfraData implements JsonSerializable, JsonDeserializableInterface, LoggableInterface { use LoggableTrait; + use JsonSerializableTrait; use JsonDeserializableTrait; } diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestInfraDataPerProcess.php b/tests/ElasticApmTests/ComponentTests/Util/TestInfraDataPerProcess.php index 7bce6b9b8..776ae9e8d 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/TestInfraDataPerProcess.php +++ b/tests/ElasticApmTests/ComponentTests/Util/TestInfraDataPerProcess.php @@ -37,6 +37,6 @@ final class TestInfraDataPerProcess extends TestInfraData /** @var string */ public $thisSpawnedProcessInternalId; - /** @var ?int */ - public $thisServerPort = null; + /** @var int[] */ + public $thisServerPorts = []; } diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php b/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php index 979ef8ef1..25d60339f 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php @@ -32,6 +32,7 @@ use ElasticApmTests\Util\LogCategoryForTests; use ErrorException; use Exception; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -39,6 +40,7 @@ use React\EventLoop\LoopInterface; use React\Http\HttpServer; use React\Promise\Promise; +use React\Socket\ConnectionInterface; use React\Socket\SocketServer; use RuntimeException; use Throwable; @@ -47,6 +49,7 @@ abstract class TestInfraHttpServerProcessBase extends SpawnedProcessBase { use HttpServerProcessTrait; + public const CLEAN_TEST_SCOPED_URI_PATH = '/clean_test_scoped'; public const EXIT_URI_PATH = '/exit'; /** @var Logger */ @@ -55,8 +58,8 @@ abstract class TestInfraHttpServerProcessBase extends SpawnedProcessBase /** @var ?LoopInterface */ protected $reactLoop = null; - /** @var ?SocketServer */ - protected $serverSocket = null; + /** @var SocketServer[] */ + protected $serverSockets = []; public function __construct() { @@ -88,12 +91,14 @@ function ( ); } + /** @inheritDoc */ protected function processConfig(): void { parent::processConfig(); - TestCase::assertNotNull( - AmbientContextForTests::testConfig()->dataPerProcess->thisServerPort, + Assert::assertCount( + $this->expectedPortsCount(), + AmbientContextForTests::testConfig()->dataPerProcess->thisServerPorts, LoggableToString::convert(AmbientContextForTests::testConfig()) ); @@ -101,6 +106,35 @@ protected function processConfig(): void TestCase::assertNull(AmbientContextForTests::testConfig()->dataPerRequest); } + /** + * @return int + */ + protected function expectedPortsCount(): int + { + return 1; + } + + /** + * @param int $socketIndex + * @param ConnectionInterface $connection + * + * @return void + */ + protected function onNewConnection(int $socketIndex, ConnectionInterface $connection): void + { + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + 'New connection', + [ + 'socketIndex' => $socketIndex, + 'connection addresses' => [ + 'remote' => $connection->getRemoteAddress(), + 'local' => $connection->getLocalAddress(), + ] + ] + ); + } + /** * @param ServerRequestInterface $request * @@ -131,34 +165,45 @@ public function runImpl(): void private function runHttpServer(): void { $this->reactLoop = Loop::get(); - $thisServerPort = AmbientContextForTests::testConfig()->dataPerProcess->thisServerPort; - TestCase::assertNotNull($thisServerPort); - $uri = HttpServerHandle::DEFAULT_HOST . ':' . $thisServerPort; - $this->serverSocket = new SocketServer($uri, /* context */ [], $this->reactLoop); - $httpServer = new HttpServer( - /** - * @param ServerRequestInterface $request - * - * @return ResponseInterface|Promise - */ - function (ServerRequestInterface $request) { - return $this->processRequestWrapper($request); - } - ); - $httpServer->listen($this->serverSocket); - - ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log( - 'Waiting for incoming requests...', - ['serverSocketAddress' => $this->serverSocket->getAddress()] - ); + TestCase::assertNotEmpty(AmbientContextForTests::testConfig()->dataPerProcess->thisServerPorts); + foreach (AmbientContextForTests::testConfig()->dataPerProcess->thisServerPorts as $port) { + $uri = HttpServerHandle::DEFAULT_HOST . ':' . $port; + $serverSocket = new SocketServer($uri, /* context */ [], $this->reactLoop); + $socketIndex = count($this->serverSockets); + $this->serverSockets[] = $serverSocket; + $serverSocket->on( + 'connection' /* <- event */, + function (ConnectionInterface $connection) use ($socketIndex): void { + $this->onNewConnection($socketIndex, $connection); + } + ); + $httpServer = new HttpServer( + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface|Promise + */ + function (ServerRequestInterface $request) { + return $this->processRequestWrapper($request); + } + ); + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + 'Listening for incoming requests...', + ['serverSocket address' => $serverSocket->getAddress()] + ); + $httpServer->listen($serverSocket); + } $this->beforeLoopRun(); - TestCase::assertNotNull($this->reactLoop); + Assert::assertNotNull($this->reactLoop); $this->reactLoop->run(); } + /** + * @return void + */ protected function beforeLoopRun(): void { } @@ -253,7 +298,10 @@ function (string $headerName) use ($request): ?string { return $response; } - return self::buildErrorResponse(400, 'Unknown URI path: `' . $request->getRequestTarget() . '\''); + return self::buildErrorResponse( + HttpConstantsForTests::STATUS_BAD_REQUEST, + 'Unknown URI path: `' . $request->getRequestTarget() . '\'' + ); } /** @@ -261,8 +309,9 @@ function (string $headerName) use ($request): ?string { */ protected function exit(): void { - TestCase::assertNotNull($this->serverSocket); - $this->serverSocket->close(); + foreach ($this->serverSockets as $serverSocket) { + $serverSocket->close(); + } ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Exiting...'); diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerStarter.php b/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerStarter.php index 290108816..3bd6752e8 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerStarter.php +++ b/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerStarter.php @@ -45,9 +45,13 @@ public static function startTestInfraHttpServer( string $dbgServerDesc, string $runScriptName, array $portsInUse, + int $portsToAllocateCount, ?ResourcesCleanerHandle $resourcesCleaner ): HttpServerHandle { - return (new self($dbgServerDesc, $runScriptName, $resourcesCleaner))->startHttpServer($portsInUse); + return (new self($dbgServerDesc, $runScriptName, $resourcesCleaner))->startHttpServer( + $portsInUse, + $portsToAllocateCount + ); } /** @@ -67,18 +71,18 @@ private function __construct( } /** @inheritDoc */ - protected function buildCommandLine(int $port): string + protected function buildCommandLine(array $ports): string { return 'php ' . '"' . FileUtilForTests::listToPath([__DIR__, $this->runScriptName]) . '"'; } /** @inheritDoc */ - protected function buildEnvVars(string $spawnedProcessInternalId, int $port): array + protected function buildEnvVars(string $spawnedProcessInternalId, array $ports): array { return InfraUtilForTests::addTestInfraDataPerProcessToEnvVars( EnvVarUtilForTests::getAll() /* <- baseEnvVars */, $spawnedProcessInternalId, - $port, + $ports, $this->resourcesCleaner, $this->dbgServerDesc ); diff --git a/tests/ElasticApmTests/Util/ArrayUtilForTests.php b/tests/ElasticApmTests/Util/ArrayUtilForTests.php index f15be6e8a..3a7e5d42d 100644 --- a/tests/ElasticApmTests/Util/ArrayUtilForTests.php +++ b/tests/ElasticApmTests/Util/ArrayUtilForTests.php @@ -25,7 +25,7 @@ use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Util\StaticClassTrait; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Assert; final class ArrayUtilForTests { @@ -48,16 +48,18 @@ public static function getFirstValue(array $array) */ public static function getSingleValue(array $array) { - TestCase::assertCount(1, $array); + Assert::assertCount(1, $array); return self::getFirstValue($array); } /** * @template T + * * @param T[] $array + * * @return T */ - public static function getLastValue(array $array) + public static function &getLastValue(array $array) { return $array[count($array) - 1]; } @@ -69,7 +71,7 @@ public static function getLastValue(array $array) */ public static function addUnique($key, $value, array &$result): void { - TestCase::assertArrayNotHasKey( + Assert::assertArrayNotHasKey( $key, $result, LoggableToString::convert(['key' => $key, 'value' => $value, 'result' => $result]) @@ -115,4 +117,37 @@ public static function iterateListInReverse(array $array): iterable yield $array[$i]; } } + + /** + * @template TKey of string|int + * @template TValue + * + * @param array $from + * @param array $to + */ + public static function append(array $from, array &$to): void + { + $to = array_merge($to, $from); + } + + /** + * @template TKey + * @template TValue + * + * @param array $map + * @param TKey $keyToFind + * + * @return int + */ + public static function getAdditionOrderIndex(array $map, $keyToFind): int + { + $additionOrderIndex = 0; + foreach ($map as $key => $ignored) { + if ($key === $keyToFind) { + return $additionOrderIndex; + } + ++$additionOrderIndex; + } + Assert::fail('Not found key in map; ' . LoggableToString::convert(['keyToFind' => $keyToFind, 'map' => $map])); + } } diff --git a/tests/ElasticApmTests/Util/AssertMessageBuilder.php b/tests/ElasticApmTests/Util/AssertMessageBuilder.php new file mode 100644 index 000000000..b861323d6 --- /dev/null +++ b/tests/ElasticApmTests/Util/AssertMessageBuilder.php @@ -0,0 +1,83 @@ + */ + private $ctx; + + /** + * @param array $initialCtx + */ + public function __construct(array $initialCtx = []) + { + $this->ctx = $initialCtx; + } + + /** + * @param string $key + * @param mixed $value + * + * @return void + */ + public function add(string $key, $value): void + { + $this->ctx[$key] = $value; + } + + /** + * @param array $from + * + * @return void + */ + public function append(array $from): void + { + $this->ctx = array_merge($this->ctx, $from); + } + + /** + * @param array $additionalCtx + * + * @return self + */ + public function inherit(array $additionalCtx = []): self + { + return new self(array_merge($this->ctx, $additionalCtx)); + } + + public function s(): string + { + return LoggableToString::convert($this->ctx); + } + + /** @inheritDoc */ + public function __toString(): string + { + return LoggableToString::convert($this->ctx); + } +} diff --git a/tests/ElasticApmTests/Util/DataFromAgent.php b/tests/ElasticApmTests/Util/DataFromAgent.php index 18bb4f701..eb5ccb80c 100644 --- a/tests/ElasticApmTests/Util/DataFromAgent.php +++ b/tests/ElasticApmTests/Util/DataFromAgent.php @@ -60,8 +60,7 @@ class DataFromAgent implements LoggableInterface */ private static function singleEvent(array $events) { - TestCase::assertCount(1, $events); - return ArrayUtilForTests::getFirstValue($events); + return ArrayUtilForTests::getSingleValue($events); } /** diff --git a/tests/ElasticApmTests/Util/DataFromAgentExpectations.php b/tests/ElasticApmTests/Util/DataFromAgentExpectations.php index 924c045c5..cd7874718 100644 --- a/tests/ElasticApmTests/Util/DataFromAgentExpectations.php +++ b/tests/ElasticApmTests/Util/DataFromAgentExpectations.php @@ -25,15 +25,15 @@ class DataFromAgentExpectations extends ExpectationsBase { - /** @var ErrorExpectations */ - public $error; + /** @var ErrorExpectations[] */ + public $errors = []; /** @var array */ - public $agentEphemeralIdToMetadata; + public $agentEphemeralIdToMetadata = []; - /** @var MetricSetExpectations */ - public $metricSet; + /** @var MetricSetExpectations[] */ + public $metricSets = []; - /** @var TraceExpectations */ - public $trace; + /** @var TraceExpectations[] */ + public $traces = []; } diff --git a/tests/ElasticApmTests/Util/DataFromAgentValidator.php b/tests/ElasticApmTests/Util/DataFromAgentValidator.php index 9b565cf78..28afbb825 100644 --- a/tests/ElasticApmTests/Util/DataFromAgentValidator.php +++ b/tests/ElasticApmTests/Util/DataFromAgentValidator.php @@ -23,7 +23,10 @@ namespace ElasticApmTests\Util; -use PHPUnit\Framework\TestCase; +use Elastic\Apm\Impl\Log\LoggableToString; +use Elastic\Apm\Impl\Util\ArrayUtil; +use Elastic\Apm\Impl\Util\RangeUtil; +use PHPUnit\Framework\Assert; class DataFromAgentValidator { @@ -48,27 +51,15 @@ private function __construct(DataFromAgentExpectations $expectations, DataFromAg private function validateImpl(): void { - foreach ($this->actual->idToError as $error) { - $error->assertMatches($this->expectations->error); - } + $tracesInOrderReceived = $this->splitIntoTracesInOrderReceived(); + $traceIdsInOrderReceived = self::getTraceIdsInOrderReceived($tracesInOrderReceived); + $this->validateTraces($tracesInOrderReceived); - foreach ($this->actual->metadatas as $metadata) { - TestCase::assertNotNull($metadata->service->agent); - $agentEphemeralId = $metadata->service->agent->ephemeralId; - TestCase::assertNotNull($agentEphemeralId); - self::assertValidNullableKeywordString($agentEphemeralId); - TestCase::assertArrayHasKey($agentEphemeralId, $this->expectations->agentEphemeralIdToMetadata); - MetadataValidator::assertValid( - $metadata, - $this->expectations->agentEphemeralIdToMetadata[$agentEphemeralId] - ); - } + $this->validateErrors($traceIdsInOrderReceived); - foreach ($this->actual->metricSets as $metricSet) { - MetricSetValidator::assertValid($metricSet, $this->expectations->metricSet); - } + $this->validateMetadatas(); - $this->validateTraces(); + $this->validateMetrics(); } /** @@ -82,26 +73,129 @@ private static function groupByTraceId(array $idToExecSegments): array { $result = []; foreach ($idToExecSegments as $execSegment) { - if (!array_key_exists($execSegment->traceId, $result)) { - $result[$execSegment->traceId] = []; - } - $result[$execSegment->traceId][$execSegment->id] = $execSegment; + $idToExecSegmentsForTraceId =& ArrayUtil::getOrAdd($execSegment->traceId, /* defaultValue */ [], $result); + ArrayUtilForTests::addUnique($execSegment->id, $execSegment, /* ref */ $idToExecSegmentsForTraceId); } return $result; } - private function validateTraces(): void + /** + * @return TraceActual[] + */ + private function splitIntoTracesInOrderReceived(): array { + $orderReceivedIndexToTrace = []; + $transactionsInOrderReceived = array_values($this->actual->idToTransaction); $transactionsByTraceId = self::groupByTraceId($this->actual->idToTransaction); $spansByTraceId = self::groupByTraceId($this->actual->idToSpan); TestCaseBase::assertListArrayIsSubsetOf(array_keys($spansByTraceId), array_keys($transactionsByTraceId)); foreach ($transactionsByTraceId as $traceId => $idToTransaction) { - TestCase::assertIsArray($idToTransaction); + Assert::assertIsArray($idToTransaction); /** @var array $idToTransaction */ $idToSpan = array_key_exists($traceId, $spansByTraceId) ? $spansByTraceId[$traceId] : []; - TestCase::assertIsArray($idToSpan); + Assert::assertIsArray($idToSpan); /** @var array $idToSpan */ - TraceValidator::validate(new TraceActual($idToTransaction, $idToSpan), $this->expectations->trace); + + $trace = new TraceActual($idToTransaction, $idToSpan); + $orderReceivedIndex + = array_search($trace->rootTransaction, $transactionsInOrderReceived, /* strict */ true); + Assert::assertIsInt($orderReceivedIndex); + ArrayUtilForTests::addUnique($orderReceivedIndex, $trace, /* ref */ $orderReceivedIndexToTrace); + } + ksort(/* ref*/ $orderReceivedIndexToTrace); + return array_values($orderReceivedIndexToTrace); + } + + /** + * @param TraceActual[] $tracesInOrderReceived + * + * @return void + */ + private function validateTraces(array $tracesInOrderReceived): void + { + $ctx = ['expectations->traces' => $this->expectations->traces]; + $ctx['tracesInOrderReceived'] = $tracesInOrderReceived; + $ctxAsStr = LoggableToString::convert($ctx); + Assert::assertSame(count($this->expectations->traces), count($tracesInOrderReceived), $ctxAsStr); + foreach (RangeUtil::generateUpTo(count($tracesInOrderReceived)) as $indexOrderReceived) { + $traceExpectations = $this->expectations->traces[$indexOrderReceived]; + TraceValidator::validate($tracesInOrderReceived[$indexOrderReceived], $traceExpectations); + } + } + + /** + * @param TraceActual[] $tracesInOrderReceived + * + * @return string[] + */ + private static function getTraceIdsInOrderReceived(array $tracesInOrderReceived): array + { + $result = []; + foreach ($tracesInOrderReceived as $trace) { + $result[] = $trace->rootTransaction->traceId; + } + return $result; + } + + /** + * @param string[] $traceIdsInOrderReceived + * + * @return void + */ + private function validateErrors(array $traceIdsInOrderReceived): void + { + if (ArrayUtil::isEmpty($this->actual->idToError)) { + return; + } + + $orderTraceReceivedIndexToErrors = []; + foreach ($this->actual->idToError as $error) { + $orderTraceReceivedIndex = array_search($error->traceId, $traceIdsInOrderReceived, /* strict */ true); + Assert::assertIsInt($orderTraceReceivedIndex); + $errors =& ArrayUtil::getOrAdd( + $orderTraceReceivedIndex, + [] /* <- defaultValue */, + $orderTraceReceivedIndexToErrors /* <- ref */ + ); + $errors[] = $error; + } + + $msg = new AssertMessageBuilder(['expectations->errors' => $this->expectations->errors]); + $msg->add('orderTraceReceivedIndexToErrors', $orderTraceReceivedIndexToErrors); + Assert::assertSame(count($orderTraceReceivedIndexToErrors), count($this->expectations->errors), $msg->s()); + foreach (RangeUtil::generateUpTo(count($orderTraceReceivedIndexToErrors)) as $indexOrderReceived) { + $errorExpectations = $this->expectations->errors[$indexOrderReceived]; + $errors = $orderTraceReceivedIndexToErrors[$indexOrderReceived]; + foreach ($errors as $error) { + $error->assertMatches($errorExpectations); + } + } + } + + private function validateMetadatas(): void + { + foreach ($this->actual->metadatas as $metadata) { + Assert::assertNotNull($metadata->service->agent); + $agentEphemeralId = $metadata->service->agent->ephemeralId; + Assert::assertNotNull($agentEphemeralId); + self::assertValidNullableKeywordString($agentEphemeralId); + Assert::assertArrayHasKey($agentEphemeralId, $this->expectations->agentEphemeralIdToMetadata); + MetadataValidator::assertValid( + $metadata, + $this->expectations->agentEphemeralIdToMetadata[$agentEphemeralId] + ); + } + } + + private function validateMetrics(): void + { + $firstExpectations = ArrayUtilForTests::getFirstValue($this->expectations->metricSets); + $lastExpectations = ArrayUtilForTests::getLastValue($this->expectations->metricSets); + $combinedExpectations = new MetricSetExpectations(); + $combinedExpectations->timestampBefore = $firstExpectations->timestampBefore; + $combinedExpectations->timestampAfter = $lastExpectations->timestampAfter; + foreach ($this->actual->metricSets as $metricSet) { + MetricSetValidator::assertValid($metricSet, $combinedExpectations); } } } diff --git a/tests/ElasticApmTests/Util/Deserialization/JsonDeserializableTrait.php b/tests/ElasticApmTests/Util/Deserialization/JsonDeserializableTrait.php index 834ad5561..fb668e0b4 100644 --- a/tests/ElasticApmTests/Util/Deserialization/JsonDeserializableTrait.php +++ b/tests/ElasticApmTests/Util/Deserialization/JsonDeserializableTrait.php @@ -26,37 +26,22 @@ use Elastic\Apm\Impl\BackendComm\SerializationUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\JsonUtil; -use PHPUnit\Framework\TestCase; +use ElasticApmTests\Util\AssertMessageBuilder; +use PHPUnit\Framework\Assert; trait JsonDeserializableTrait { - /** - * @return array - * - * Called by json_encode - * @noinspection PhpUnused - */ - public function jsonSerialize(): array - { - $result = []; - - $className = ClassNameUtil::fqToShort(get_class($this)); - foreach (get_object_vars($this) as $propName => $propValue) { - JsonUtilForTests::assertJsonDeserializable($propValue, $className . '->' . $propName); - $result[$propName] = $propValue; - } - - return $result; - } - /** * @param array $decodedJson */ public function deserializeFromDecodedJson(array $decodedJson): void { + $msgBeforeIt = new AssertMessageBuilder(['decodedJson' => $decodedJson]); foreach ($decodedJson as $jsonKey => $jsonVal) { - TestCase::assertIsString($jsonKey); - TestCase::assertTrue(property_exists($this, $jsonKey)); + $thisClassName = ClassNameUtil::fqToShort(get_called_class()); + $msg = $msgBeforeIt->inherit(['jsonKey' => $jsonKey, 'this class' => $thisClassName]); + Assert::assertIsString($jsonKey, $msg->s()); + Assert::assertTrue(property_exists($this, $jsonKey), $msg->s()); $this->$jsonKey = $this->deserializePropertyValue($jsonKey, $jsonVal); } } diff --git a/tests/ElasticApmTests/Util/Deserialization/JsonSerializableTrait.php b/tests/ElasticApmTests/Util/Deserialization/JsonSerializableTrait.php new file mode 100644 index 000000000..2f15a2cc1 --- /dev/null +++ b/tests/ElasticApmTests/Util/Deserialization/JsonSerializableTrait.php @@ -0,0 +1,48 @@ + + * + * Called by json_encode + * @noinspection PhpUnused + */ + public function jsonSerialize(): array + { + $result = []; + + $className = ClassNameUtil::fqToShort(get_class($this)); + foreach (get_object_vars($this) as $propName => $propValue) { + JsonUtilForTests::assertJsonDeserializable($propValue, $className . '->' . $propName); + $result[$propName] = $propValue; + } + + return $result; + } +} diff --git a/tests/ElasticApmTests/Util/TraceActual.php b/tests/ElasticApmTests/Util/TraceActual.php index 68a658469..1ec816995 100644 --- a/tests/ElasticApmTests/Util/TraceActual.php +++ b/tests/ElasticApmTests/Util/TraceActual.php @@ -23,8 +23,14 @@ namespace ElasticApmTests\Util; +use Elastic\Apm\Impl\Log\LoggableToString; +use PHPUnit\Framework\Assert; + final class TraceActual { + /** @var TransactionDto */ + public $rootTransaction; + /** @var array */ public $idToTransaction; @@ -37,7 +43,31 @@ final class TraceActual */ public function __construct(array $idToTransaction, array $idToSpan) { + $this->rootTransaction = self::findRootTransaction($idToTransaction); $this->idToTransaction = $idToTransaction; $this->idToSpan = $idToSpan; } + + /** + * @param array $idToTransaction + * + * @return TransactionDto + */ + public static function findRootTransaction(array $idToTransaction): TransactionDto + { + /** @var ?TransactionDto $rootTransaction */ + $rootTransaction = null; + foreach ($idToTransaction as $currentTransaction) { + if ($currentTransaction->parentId === null) { + Assert::assertNull($rootTransaction, 'Found more than one root transaction'); + $rootTransaction = $currentTransaction; + } + } + Assert::assertNotNull( + $rootTransaction, + 'Root transaction not found. ' . LoggableToString::convert(['idToTransaction' => $idToTransaction]) + ); + /** @var TransactionDto $rootTransaction */ + return $rootTransaction; + } } diff --git a/tests/ElasticApmTests/Util/TraceValidator.php b/tests/ElasticApmTests/Util/TraceValidator.php index fd837d903..3f26615be 100644 --- a/tests/ElasticApmTests/Util/TraceValidator.php +++ b/tests/ElasticApmTests/Util/TraceValidator.php @@ -26,7 +26,6 @@ use Ds\Queue; use Ds\Set; use Elastic\Apm\Impl\Constants; -use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\UrlParts; use Elastic\Apm\Impl\Util\UrlUtil; @@ -59,7 +58,7 @@ protected function validateImpl(): void { $idToTransaction = $this->actual->idToTransaction; $idToSpan = $this->actual->idToSpan; - $rootTransaction = self::findRootTransaction($idToTransaction); + $rootTransaction = $this->actual->rootTransaction; if ($this->expectations->shouldVerifyRootTransaction) { $this->validateRootTransaction($rootTransaction); } @@ -192,29 +191,6 @@ private static function assertGraphIsTree(string $rootId, array $idToParentId): TestCase::assertCount($idsReachableFromRoot->count(), $idToParentId); } - /** - * @param array $idToTransaction - * - * @return TransactionDto - */ - public static function findRootTransaction(array $idToTransaction): TransactionDto - { - /** @var ?TransactionDto $rootTransaction */ - $rootTransaction = null; - foreach ($idToTransaction as $currentTransaction) { - if ($currentTransaction->parentId === null) { - TestCase::assertNull($rootTransaction, 'Found more than one root transaction'); - $rootTransaction = $currentTransaction; - } - } - TestCase::assertNotNull( - $rootTransaction, - 'Root transaction not found. ' . LoggableToString::convert(['idToTransaction' => $idToTransaction]) - ); - /** @var TransactionDto $rootTransaction */ - return $rootTransaction; - } - private function assertTransactionsGraphIsTree(TransactionDto $rootTransaction): void { /** @var array $transactionIdToParentId */ diff --git a/tests/polyfills/Stringable.php b/tests/polyfills/Stringable.php new file mode 100644 index 000000000..09c705ed0 --- /dev/null +++ b/tests/polyfills/Stringable.php @@ -0,0 +1,49 @@ +