diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b80cb675..4c5a6c03 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -302,6 +302,111 @@ jobs: require_tests: true annotate_only: true + server_linux_x86_64: + needs: + - source + - build_linux_x86_64 + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + php: + - '8.3' + ts: + - nts + - zts + server: + - 7.6.0 + - 7.2.4 + - 7.1.6 + - 7.0.5 + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, mbstring, intl + env: + phpts: ${{ matrix.ts }} + - name: Install cbdinocluster + run: | + mkdir -p "$HOME/bin" + curl -L -o "$HOME/bin/cbdinocluster" https://github.com/couchbaselabs/cbdinocluster/releases/download/v0.0.35/cbdinocluster-linux + chmod a+x "$HOME/bin/cbdinocluster" + echo "$HOME/bin" >> $GITHUB_PATH + - name: Initialize cbdinocluster + run: | + cbdinocluster -v init --auto + - name: Start couchbase cluster + env: + CLUSTERCONFIG: | + nodes: + - count: 2 + version: ${{ matrix.server }} + services: + - kv + - n1ql + - index + - count: 1 + version: ${{ matrix.server }} + services: + - kv + - fts + - cbas + docker: + kv-memory: 1600 + run: | + CLUSTER_ID=$(cbdinocluster -v allocate --def="${CLUSTERCONFIG}") + CONNECTION_STRING=$(cbdinocluster -v connstr "${CLUSTER_ID}") + cbdinocluster -v buckets add ${CLUSTER_ID} default --ram-quota-mb=100 --flush-enabled=true + cbdinocluster -v buckets load-sample ${CLUSTER_ID} travel-sample + echo "CLUSTER_ID=${CLUSTER_ID}" >> "$GITHUB_ENV" + echo "TEST_CONNECTION_STRING=${CONNECTION_STRING}?dump_configuration=true" >> "$GITHUB_ENV" + - uses: actions/download-artifact@v4 + with: + name: couchbase-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + name: scripts-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + path: tests + name: tests-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + name: couchbase-${{ needs.source.outputs.extension_version }}-php${{ matrix.php }}-${{ matrix.ts }}-linux-x86_64 + - name: Test + timeout-minutes: 60 + env: + TEST_SERVER_VERSION: "${{ matrix.server }}" + TEST_LOG_LEVEL: trace + TEST_BUCKET: default + TEST_USE_WAN_DEVELOPMENT_PROFILE: true + OTHER_TEST_BUCKET: secBucket + run: | + tar xf couchbase-*-linux-x86_64.tgz + ruby ./bin/test.rb + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4.1.0 + if: always() + with: + check_name: 🐧server, php-${{ matrix.php }}-${{ matrix.ts }} + report_paths: results.xml + require_tests: true + annotate_only: true + - name: Collect server logs + timeout-minutes: 15 + if: failure() + run: | + cbdinocluster -v collect-logs $CLUSTER_ID ./logs + - name: Upload logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.php }}-${{ matrix.ts }}-${{ matrix.server }}-logs + path: | + logs/* + build_macos_x86_64: needs: source runs-on: macos-13 @@ -509,3 +614,126 @@ jobs: report_paths: results.xml require_tests: true annotate_only: true + + build_windows: + needs: source + runs-on: windows-2019 + strategy: + fail-fast: false + matrix: + php: + - '8.3.4' + - '8.2.17' + - '8.1.27' + ts: + - nts + - zts + arch: + - x64 + - x86 + steps: + - name: Install dependencies + shell: cmd + run: | + # winget install Git.Git + # winget install Kitware.CMake + # winget install NASM.NASM + choco install nasm + git clone --no-progress https://github.com/php/php-sdk-binary-tools.git c:\php\php-sdk + git clone --no-progress --depth 1 --branch php-${{ matrix.php }} https://github.com/php/php-src.git c:\php\php-src + - uses: actions/download-artifact@v4 + with: + path: c:\php + name: couchbase-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + path: c:\php + name: scripts-${{ needs.source.outputs.extension_version }} + - name: Build + working-directory: c:\php + shell: cmd + run: | + 7z x couchbase-${{ needs.source.outputs.extension_version }}.tgz -so | 7z x -aoa -si -ttar + cd c:\php\php-src + echo call buildconf.bat --add-modules-dir=c:\php > task.bat + echo call configure.bat --disable-all --enable-cli ${{ matrix.ts == 'nts' && '--disable-zts' || '' }} --enable-couchbase >> task.bat + echo nmake >> task.bat + call c:\php\php-sdk\phpsdk-vs16-${{ matrix.arch }}.bat -t task.bat + exit /b %ERRORLEVEL% + - name: Package + id: package + working-directory: c:\php + run: | + $PhpVersion = ("${{ matrix.php }}" -split '\.')[0..1] -join '.' + Add-Content -Path $env:GITHUB_OUTPUT -Value "php_version=$PhpVersion" + $SourceDirectory = (Get-ChildItem -Path "c:\php" -Directory "couchbase-*" | Select-Object -First 1).FullName + $DistName = "couchbase-${{ needs.source.outputs.extension_version }}-php${PhpVersion}-${{ matrix.ts }}-windows-${{ matrix.arch }}" + New-Item -ItemType Directory -Path $DistName | Out-Null + $FilesToCopy = Get-ChildItem -Path . -Filter "couchbase-*" -Include LICENSE,"Couchbase","GPBMetadata" + Copy-Item -Path 90-couchbase.ini -Destination $DistName -Force + Copy-Item -Path "${SourceDirectory}\LICENSE" -Destination $DistName -Force + Copy-Item -Path "${SourceDirectory}\Couchbase" -Destination $DistName -Force -Recurse + Copy-Item -Path "${SourceDirectory}\GPBMetadata" -Destination $DistName -Force -Recurse + $FilesToCopy = Get-ChildItem -Path "C:\php\php-src\${{ matrix.arch == 'x64' && 'x64\\' || '' }}Release${{ matrix.ts == 'zts' && '_TS' || '' }}" -Filter "php_couchbase.*" + foreach ($File in $FilesToCopy) { + Write-Host "Copying file: $($File.FullName)" + Copy-Item -Path $File.FullName -Destination $DistName -Force + } + $ZipArchive = $DistName + ".zip" + Write-Host "Compressing $DistName directory into $ZipArchive" + Compress-Archive -Path $DistName -DestinationPath $ZipArchive -CompressionLevel Optimal + - uses: actions/upload-artifact@v4 + with: + name: couchbase-${{ needs.source.outputs.extension_version }}-php${{ steps.package.outputs.php_version }}-${{ matrix.ts }}-windows-${{ matrix.arch }} + path: | + c:\php\couchbase-*-windows-${{ matrix.arch }}.zip + + mock_windows: + needs: + - source + - build_windows + runs-on: windows-2019 + strategy: + fail-fast: false + matrix: + php: + - '8.1' + - '8.2' + - '8.3' + ts: + - nts + - zts + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, mbstring, intl, sockets + env: + phpts: ${{ matrix.ts }} + - uses: actions/download-artifact@v4 + with: + name: couchbase-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + name: scripts-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + path: tests + name: tests-${{ needs.source.outputs.extension_version }} + - uses: actions/download-artifact@v4 + with: + name: couchbase-${{ needs.source.outputs.extension_version }}-php${{ matrix.php }}-${{ matrix.ts }}-windows-x64 + - name: Test + timeout-minutes: 60 + run: | + Expand-Archive -Path couchbase-*-windows-x64.zip + ruby ./bin/test.rb + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4.1.0 + if: always() + with: + check_name: 🪟caves, php-${{ matrix.php }}-${{ matrix.ts }} + report_paths: results.xml + require_tests: true + annotate_only: true diff --git a/.gitignore b/.gitignore index d50b31d2..b35f46d8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ .deps .libs/ .phpdoc +.cache /*.out +/logs /.idea/ /.phpunit.result.cache /Debug/ diff --git a/bin/test.rb b/bin/test.rb index 3d3f9283..3a382e35 100755 --- a/bin/test.rb +++ b/bin/test.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true # Copyright 2020-Present Couchbase, Inc. # @@ -14,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +require "English" require "fileutils" require "rbconfig" require "shellwords" @@ -30,7 +32,6 @@ end CB_PHP_PREFIX = ENV.fetch("CB_PHP_PREFIX", DEFAULT_PHP_PREFIX) -CB_PHP_EXECUTABLE = ENV.fetch("CB_PHP_EXECUTABLE", File.join(CB_PHP_PREFIX, "bin", CB_PHP_NAME)) def which(name) ENV.fetch("PATH", "") @@ -39,16 +40,38 @@ def which(name) .find { |file| File.executable?(file) } end +def windows? + RbConfig::CONFIG["target_os"].match?(/mingw/) +end + +CB_PHP_EXECUTABLE = + if windows? + which("php") + else + ENV.fetch("CB_PHP_EXECUTABLE", File.join(CB_PHP_PREFIX, "bin", CB_PHP_NAME)) + end + def run(*args) - args = args.compact.map(&:to_s) - puts args.join(" ") - system(*args) || abort("command returned non-zero status (#{$?}): #{args.join(" ")}") + puts + args.flatten.compact! + pp args + system(*args) || abort("command returned non-zero status (#{$CHILD_STATUS}): #{args.join(' ')}") +end + +def capture(*args) + puts + args.flatten.compact! + pp args + command = args.join(" ") + output = IO.popen(args, err: [:child, :out], &:read) + abort("command returned non-zero status (#{$CHILD_STATUS}): #{command}") unless $CHILD_STATUS.success? + output end project_root = File.expand_path(File.join(__dir__, "..")) build_root = File.join(project_root, "build") -caves_binary = File.join(build_root, "gocaves") +caves_binary = File.join(build_root, windows? ? "gocaves.exe" : "gocaves") unless File.file?(caves_binary) caves_version = "v0.0.1-78" basename = @@ -67,14 +90,18 @@ def run(*args) else "gocaves-linux-amd64" end + when /mingw/ + "gocaves-windows.exe" else - abort(format("unexpected architecture, please update \"%s\", your target_os=\"%s\", arch=\"%s\"", - File.realpath(__FILE__), RbConfig::CONFIG["target_os"], RbConfig::CONFIG["arch"])) + abort(format('unexpected architecture, please update "%s", your target_os="%s", arch="%s"', + this_file: File.realpath(__FILE__), + os: RbConfig::CONFIG["target_os"], + arch: RbConfig::CONFIG["arch"])) end caves_url = "https://github.com/couchbaselabs/gocaves/releases/download/#{caves_version}/#{basename}" FileUtils.mkdir_p(File.dirname(caves_binary)) run("curl -L -o #{caves_binary.shellescape} #{caves_url}") - run("chmod a+x #{caves_binary.shellescape}") + run("chmod a+x #{caves_binary.shellescape}") unless windows? end php_unit_phar = File.join(build_root, "phpunit.phar") @@ -86,15 +113,15 @@ def run(*args) end module_names = [ - "couchbase.#{RbConfig::CONFIG["DLEXT"]}", - "couchbase.#{RbConfig::CONFIG["SOEXT"]}", + "couchbase.#{RbConfig::CONFIG['DLEXT']}", + "couchbase.#{RbConfig::CONFIG['SOEXT']}", "couchbase.so", -] +].map { |name| ["php_#{name}", name] }.flatten module_locations = module_names.map do |name| [ "#{project_root}/modules/#{name}", "#{project_root}/#{name}", - ] + Dir["#{project_root}/couchbase*/#{name}"] + ] + Dir["#{project_root}/couchbase*/**/#{name}"] end.flatten.sort.uniq couchbase_ext = module_locations.find { |path| File.exist?(path) } @@ -108,8 +135,43 @@ def run(*args) tests << File.join(project_root, "tests") if tests.empty? results_xml = File.join(project_root, "results.xml") +logs_dir = File.join(project_root, "logs") +FileUtils.mkdir_p(logs_dir) + +log_args = + if ENV["CI"] + [ + "-d", "couchbase.log_stderr=0", + "-d", "couchbase.log_path=#{File.join(logs_dir, 'tests.log').shellescape}" + ] + else + ["-d", "couchbase.log_stderr=1"] + end + +if (log_level = ENV.fetch("TEST_LOG_LEVEL", nil)) + log_args << "-d" << "couchbase.log_level=#{log_level}" +end + +extra_php_args = [] +if windows? + extra_php_args << "-d" << "extension=sockets" + extra_php_args << "-d" << "extension=mbstring" +end + Dir.chdir(project_root) do - run("#{CB_PHP_EXECUTABLE} -d extension=#{couchbase_ext} -m") - run("#{CB_PHP_EXECUTABLE} -d extension=#{couchbase_ext} -i | grep couchbase") - run("#{CB_PHP_EXECUTABLE} -d extension=#{couchbase_ext} -d couchbase.log_stderr=1 -d cuchbase.log_php_log_err=0 #{php_unit_phar.shellescape} --color --testdox #{tests.map(&:shellescape).join(' ')} --log-junit #{results_xml}") + run(CB_PHP_EXECUTABLE, *extra_php_args, "-d", "extension=#{couchbase_ext}", "-m") + puts capture(CB_PHP_EXECUTABLE, *extra_php_args, "-d", "extension=#{couchbase_ext}", "-i") + .split("\n") + .grep(/couchbase/i) + .join("\n") + run(CB_PHP_EXECUTABLE, + *extra_php_args, + "-d", "extension=#{couchbase_ext}", + "-d", "couchbase.log_php_log_err=0", + *log_args, + php_unit_phar, + "--color", + "--testdox", + *tests, + "--log-junit", results_xml) end diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 00000000..e179e8a3 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +cmake-build/compile_commands.json \ No newline at end of file diff --git a/config.w32 b/config.w32 index 19852c58..e4394a1c 100644 --- a/config.w32 +++ b/config.w32 @@ -7,7 +7,7 @@ if (PHP_COUCHBASE != "no") { ERROR("cmake is required (use 'winget install Kitware.CMake')"); } var LOCALAPPDATA = WshShell.Environment("Process").Item("LOCALAPPDATA"); - var NASM = PATH_PROG("nasm", LOCALAPPDATA + "\\bin\\nasm"); + var NASM = PATH_PROG("nasm", LOCALAPPDATA + "\\bin\\nasm;" + PROGRAM_FILES + "\\NASM"); if (!NASM) { ERROR("nasm is required for BoringSSL (use 'winget install NASM.NASM')"); } diff --git a/src/wrapper/connection_handle.cxx b/src/wrapper/connection_handle.cxx index a106830c..d36b32b4 100644 --- a/src/wrapper/connection_handle.cxx +++ b/src/wrapper/connection_handle.cxx @@ -20,7 +20,7 @@ #include "common.hxx" #include "connection_handle.hxx" #include "conversion_utilities.hxx" -#include "core/utils/json.hxx" +#include "logger.hxx" #include "passthrough_transcoder.hxx" #include "version.hxx" @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include @@ -581,15 +583,21 @@ class connection_handle::impl : public std::enable_shared_from_thisctx_.run(); }); break; case fork_event::child: + initialize_logger(); + CB_LOG_INFO("Resume child after fork()"); ctx_.notify_fork(asio::execution_context::fork_child); ctx_.restart(); worker_ = std::thread([self = shared_from_this()]() { self->ctx_.run(); }); diff --git a/src/wrapper/logger.cxx b/src/wrapper/logger.cxx index d4809b6e..cc237f8a 100644 --- a/src/wrapper/logger.cxx +++ b/src/wrapper/logger.cxx @@ -159,11 +159,10 @@ initialize_logger() couchbase::core::logger::configuration configuration{}; if (const char* ini_val = COUCHBASE_G(log_path); ini_val != nullptr && std::strlen(ini_val) > 0) { configuration.filename = ini_val; + configuration.filename += fmt::format(".{}", spdlog::details::os::pid()); } - if (COUCHBASE_G(log_stderr)) { - configuration.console = true; - configuration.unit_test = true; - } + configuration.unit_test = true; + configuration.console = COUCHBASE_G(log_stderr); configuration.log_level = cbpp_log_level; if (COUCHBASE_G(log_php_log_err)) { configuration.sink = global_php_log_err_sink; diff --git a/tests/AnalyticsTest.php b/tests/AnalyticsTest.php index eb8a157e..8a1d11be 100644 --- a/tests/AnalyticsTest.php +++ b/tests/AnalyticsTest.php @@ -35,10 +35,12 @@ public function testScopeAnalyticsQuery() $this->skipIfCaves(); $this->skipIfUnsupported($this->version()->supportsCollections()); - $this->maybeCreateAnalyticsIndex("beer-sample"); + $bucketName = self::env()->bucketName(); + + $this->maybeCreateAnalyticsIndex($bucketName); $id = $this->uniqueId(); - $bucket = $this->cluster->bucket('beer-sample'); + $bucket = $this->cluster->bucket($bucketName); $collection = $bucket->defaultCollection(); $scope = $bucket->scope("_default"); $collection->upsert($id, ["bar" => 42]); @@ -46,6 +48,7 @@ public function testScopeAnalyticsQuery() $options = AnalyticsOptions::build() ->scanConsistency(AnalyticsScanConsistency::REQUEST_PLUS) ->positionalParameters([$id]); + $res = $scope->analyticsQuery("SELECT * FROM `_default` where meta().id = \$1", $options); $this->assertNotEmpty($res->rows()); $this->assertEquals(42, $res->rows()[0]["_default"]['bar']); @@ -56,17 +59,19 @@ public function testClusterAnalyticsQuery() $this->skipIfCaves(); $this->skipIfUnsupported($this->version()->supportsCollections()); - $this->maybeCreateAnalyticsIndex("beer-sample"); + $bucketName = self::env()->bucketName(); + + $this->maybeCreateAnalyticsIndex($bucketName); $id = $this->uniqueId(); - $bucket = $this->cluster->bucket('beer-sample'); + $bucket = $this->cluster->bucket($bucketName); $collection = $bucket->defaultCollection(); $collection->upsert($id, ["bar" => 42]); $options = AnalyticsOptions::build() ->scanConsistency(AnalyticsScanConsistency::REQUEST_PLUS) ->positionalParameters([$id]); - $res = $this->cluster->analyticsQuery("SELECT * FROM `beer-sample`.`_default`.`_default` where meta().id = \$1", $options); + $res = $this->cluster->analyticsQuery("SELECT * FROM `$bucketName`.`_default`.`_default` where meta().id = \$1", $options); $this->assertNotEmpty($res->rows()); $this->assertEquals(42, $res->rows()[0]["_default"]['bar']); } diff --git a/tests/BucketManagerTest.php b/tests/BucketManagerTest.php index 44f05f68..de3eaf8d 100644 --- a/tests/BucketManagerTest.php +++ b/tests/BucketManagerTest.php @@ -246,6 +246,19 @@ public function testBucketMaxExpiry() $settings->setMaxExpiry(10); $this->manager->updateBucket($settings); + $manager = $this->manager; + $bucketName = $this->bucketName; + $result = $this->retryFor( + 10, + 1000, + function () use ($manager, $bucketName) { + $result = $manager->getBucket($bucketName); + if ($result->maxExpiry() == 5) { + throw new RuntimeException("the bucket still has old maxExpiry, retrying"); + } + } + ); + $result = $this->manager->getBucket($this->bucketName); $this->assertEquals(10, $result->maxExpiry()); } diff --git a/tests/CollectionManagerTest.php b/tests/CollectionManagerTest.php index 8d0e2b0b..7471e1e0 100644 --- a/tests/CollectionManagerTest.php +++ b/tests/CollectionManagerTest.php @@ -93,7 +93,7 @@ public function testDropScopeDoesNotExist(): void $this->manager->dropScope($scopeName); } - public function testCreateCollectionDeprecatedAPI(): void + public function testCreateCollectionDeprecatedApi(): void { $collectionName = $this->uniqueId("collection"); $scopeName = $this->uniqueId("scope"); @@ -112,7 +112,7 @@ public function testCreateCollectionDeprecatedAPI(): void $this->assertTrue($found); } - public function testCreateCollectionExistsDeprecatedAPI(): void + public function testCreateCollectionExistsDeprecatedApi(): void { $collectionName = $this->uniqueId("collection"); $scopeName = $this->uniqueId("scope"); @@ -123,7 +123,7 @@ public function testCreateCollectionExistsDeprecatedAPI(): void $this->manager->createCollection($collectionSpec); } - public function testDropCollectionNotExistsDeprecatedAPI(): void + public function testDropCollectionNotExistsDeprecatedApi(): void { $this->skipIfCaves(); @@ -135,7 +135,7 @@ public function testDropCollectionNotExistsDeprecatedAPI(): void $this->manager->dropCollection($collectionSpec); } - public function testDropCollectionDeprecatedAPI(): void + public function testDropCollectionDeprecatedApi(): void { $collectionName = $this->uniqueId("collection"); $scopeName = $this->uniqueId("scope"); @@ -304,7 +304,7 @@ public function testCreateCollectionNoExpiry() $collectionName = $this->uniqueId("collection"); $scopeName = $this->uniqueId("scope"); $this->manager->createScope($scopeName); - $this->manager->createCollection($scopeName, $collectionName, CreateCollectionSettings::build(-1, false)); + $this->manager->createCollection($scopeName, $collectionName, CreateCollectionSettings::build(-1)); $selectedScope = $this->getScope($scopeName); diff --git a/tests/Helpers/Caves.php b/tests/Helpers/Caves.php index f443d893..edb51c62 100644 --- a/tests/Helpers/Caves.php +++ b/tests/Helpers/Caves.php @@ -42,17 +42,19 @@ public function start() sprintf("--control-port=%d", $this->controlPort()), ], [ - 1 => ["file", sprintf("%s/%s.out.txt", $this->buildDirectory(), $this->logPrefix), "a"], - 2 => ["file", sprintf("%s/%s.err.txt", $this->buildDirectory(), $this->logPrefix), "a"], + 1 => ["file", sprintf("%s/%s.out.txt", $this->logsDirectory(), $this->logPrefix), "a"], + 2 => ["file", sprintf("%s/%s.err.txt", $this->logsDirectory(), $this->logPrefix), "a"], ], $pipes, $this->buildDirectory(), - $env, + null, ['suppress_errors' => true] ); if (is_resource($proc)) { $started = true; $this->cavesProcess = $proc; + } else { + fprintf(STDERR, "--- %s, unable to start the process\n", $this->executablePath()); } } $this->cavesSocket = socket_accept($this->controlSocket); @@ -61,7 +63,7 @@ public function start() "--- %s, control_port: %d, logs: %s\n", $this->executablePath(), $this->controlPort(), - sprintf("%s/%s.{out,err}.txt", $this->buildDirectory(), $this->logPrefix) + sprintf("%s/%s.{out,err}.txt", $this->logsDirectory(), $this->logPrefix) ); } $helloCommand = $this->readCommand(); @@ -105,9 +107,19 @@ private function executablePath(): string return $this->buildDirectory() . "/gocaves"; } + private function projectDirectory(): string + { + return realpath(__DIR__ . "/../.."); + } + private function buildDirectory(): string { - return realpath(__DIR__ . "/../../build"); + return $this->projectDirectory() . "/build"; + } + + private function logsDirectory(): string + { + return $this->projectDirectory() . "/logs"; } private function controlPort(): int @@ -124,6 +136,7 @@ private function openControlSocket() socket_bind($this->controlSocket, "127.0.0.1"); socket_listen($this->controlSocket); socket_getsockname($this->controlSocket, $address, $this->controlPort); + fprintf(STDERR, "address=%s, port=%d\n", $address, $this->controlPort); } private function roundTripCommand($cmd) @@ -139,7 +152,14 @@ private function writeCommand($cmd) private function readCommand() { - $response = socket_read($this->cavesSocket, 10000); + $response = ""; + do { + $byte = socket_read($this->cavesSocket, 1); + if ($byte === "\0") { + break; + } + $response .= $byte; + } while (true); return json_decode(trim($response), true); } } diff --git a/tests/Helpers/CouchbaseTestCase.php b/tests/Helpers/CouchbaseTestCase.php index 129aa9bb..80a019c4 100644 --- a/tests/Helpers/CouchbaseTestCase.php +++ b/tests/Helpers/CouchbaseTestCase.php @@ -26,7 +26,10 @@ if (file_exists($defaultPath)) { include_once $defaultPath; } else { - $possibleDirs = glob(__DIR__ . '/../../couchbase-*/'); + $possibleDirs = array_merge( + glob(__DIR__ . '/../../couchbase-*/'), + glob(__DIR__ . '/../../couchbase-*/couchbase-*/') + ); foreach ($possibleDirs as $dir) { $autoloadPath = $dir . 'Couchbase/autoload.php'; if (file_exists($autoloadPath)) { @@ -73,6 +76,9 @@ public function connectCluster(?ClusterOptions $options = null): ClusterInterfac $options = new ClusterOptions(); } $options->authenticator(self::env()->buildPasswordAuthenticator()); + if (getenv("TEST_USE_WAN_DEVELOPMENT_PROFILE") == "true") { + $options->applyProfile("wan_development"); + } return Cluster::connect(self::env()->connectionString(), $options); } @@ -200,7 +206,7 @@ public function retryFor(int $failAfterSecs, int $sleepMillis, callable $fn, $me try { return $fn(); } catch (Exception $ex) { - printf("%s(%s) returned exception, will retry: %s\n", $caller, $message, $ex->getMessage()); + fprintf(STDERR, "%s(%s) returned exception, will retry: %s\n", $caller, $message, $ex->getMessage()); $endException = $ex; } @@ -299,4 +305,10 @@ protected function assertErrorCode($code, $ex) ) ); } + + protected function fixCavesTimeResolutionOnWindows() { + if (PHP_OS_FAMILY === 'Windows' && self::env()->useCaves()) { + usleep(1); + } + } } diff --git a/tests/KeyValueBinaryOperationsTest.php b/tests/KeyValueBinaryOperationsTest.php index 5e68b348..e9a4f663 100644 --- a/tests/KeyValueBinaryOperationsTest.php +++ b/tests/KeyValueBinaryOperationsTest.php @@ -38,6 +38,7 @@ public function testAppendAddsBytesToTheEndOfTheDocument() $res = $collection->upsert($id, "foo", UpsertOptions::build()->transcoder(RawBinaryTranscoder::getInstance())); $originalCas = $res->cas(); + $this->fixCavesTimeResolutionOnWindows(); $res = $collection->binary()->append($id, "bar"); $appendedCas = $res->cas(); $this->assertNotEquals($appendedCas, $originalCas); @@ -55,6 +56,7 @@ public function testPrependAddsBytesToTheBeginningOfTheDocument() $res = $collection->upsert($id, "foo", UpsertOptions::build()->transcoder(RawBinaryTranscoder::getInstance())); $originalCas = $res->cas(); + $this->fixCavesTimeResolutionOnWindows(); $res = $collection->binary()->prepend($id, "bar"); $prependedCas = $res->cas(); $this->assertNotEquals($prependedCas, $originalCas); diff --git a/tests/KeyValueCounterTest.php b/tests/KeyValueCounterTest.php index 54d3e1bf..9ff0d737 100644 --- a/tests/KeyValueCounterTest.php +++ b/tests/KeyValueCounterTest.php @@ -110,36 +110,36 @@ public function testDecrementAllowsToOverrideDeltaValue() } // CXXCBC-167 -// public function testIncrementDurabilityMajority() -// { -// $this->skipIfUnsupported($this->version()->supportsEnhancedDurability()); -// -// $key = $this->uniqueId("increment-durability-majority"); -// $collection = $this->defaultCollection(); -// $opts = IncrementOptions::build()->durabilityLevel(DurabilityLevel::MAJORITY)->initial(42); -// $res = $collection->binary()->increment($key, $opts); -// $this->assertNotNull($res->cas()); -// } -// -// public function testIncrementDurabilityMajorityAndPersist() -// { -// $this->skipIfUnsupported($this->version()->supportsEnhancedDurability()); -// -// $key = $this->uniqueId("increment-durability-majority-and-persist"); -// $collection = $this->defaultCollection(); -// $opts = IncrementOptions::build()->durabilityLevel(DurabilityLevel::MAJORITY_AND_PERSIST_TO_ACTIVE, 5)->initial(42); -// $res = $collection->binary()->increment($key, $opts); -// $this->assertNotNull($res->cas()); -// } -// -// public function testIncrementDurabilityPersistToMajority() -// { -// $this->skipIfUnsupported($this->version()->supportsEnhancedDurability()); -// -// $key = $this->uniqueId("increment-durability-persist-majority"); -// $collection = $this->defaultCollection(); -// $opts = IncrementOptions::build()->durabilityLevel(DurabilityLevel::PERSIST_TO_MAJORITY)->initial(42); -// $res = $collection->binary()->increment($key, $opts); -// $this->assertNotNull($res->cas()); -// } + public function testIncrementDurabilityMajority() + { + $this->skipIfUnsupported($this->version()->supportsEnhancedDurability()); + + $key = $this->uniqueId("increment-durability-majority"); + $collection = $this->defaultCollection(); + $opts = IncrementOptions::build()->durabilityLevel(DurabilityLevel::MAJORITY)->initial(42); + $res = $collection->binary()->increment($key, $opts); + $this->assertNotNull($res->cas()); + } + + public function testIncrementDurabilityMajorityAndPersist() + { + $this->skipIfUnsupported($this->version()->supportsEnhancedDurability()); + + $key = $this->uniqueId("increment-durability-majority-and-persist"); + $collection = $this->defaultCollection(); + $opts = IncrementOptions::build()->durabilityLevel(DurabilityLevel::MAJORITY_AND_PERSIST_TO_ACTIVE, 5)->initial(42); + $res = $collection->binary()->increment($key, $opts); + $this->assertNotNull($res->cas()); + } + + public function testIncrementDurabilityPersistToMajority() + { + $this->skipIfUnsupported($this->version()->supportsEnhancedDurability()); + + $key = $this->uniqueId("increment-durability-persist-majority"); + $collection = $this->defaultCollection(); + $opts = IncrementOptions::build()->durabilityLevel(DurabilityLevel::PERSIST_TO_MAJORITY)->initial(42); + $res = $collection->binary()->increment($key, $opts); + $this->assertNotNull($res->cas()); + } } diff --git a/tests/KeyValueGetTest.php b/tests/KeyValueGetTest.php index 18d197ff..dbe9b7ef 100644 --- a/tests/KeyValueGetTest.php +++ b/tests/KeyValueGetTest.php @@ -69,7 +69,7 @@ public function testGetWithExpiry() $opts = (GetOptions::build())->withExpiry(true); $res = $collection->get($id, $opts); // Allow a bit of extra time for server edges. - $this->assertGreaterThan($now + 8, $res->expiryTime()->getTimestamp()); + $this->assertGreaterThanOrEqual($now + 8, $res->expiryTime()->getTimestamp()); } public function testGetWithProjections() diff --git a/tests/KeyValueMultiOperationsTest.php b/tests/KeyValueMultiOperationsTest.php index 2b08f7dc..dae06539 100644 --- a/tests/KeyValueMultiOperationsTest.php +++ b/tests/KeyValueMultiOperationsTest.php @@ -70,6 +70,7 @@ public function testRemoveMulti() $resFoo = $collection->upsert($idFoo, ["value" => "foo"]); $resBar = $collection->upsert($idBar, ["value" => "bar"]); + $this->fixCavesTimeResolutionOnWindows(); $res = $collection->removeMulti([$idFoo, $idMiss, $idBar]); $this->assertCount(3, $res); diff --git a/tests/KeyValueRemoveTest.php b/tests/KeyValueRemoveTest.php index 005ceade..388f3967 100644 --- a/tests/KeyValueRemoveTest.php +++ b/tests/KeyValueRemoveTest.php @@ -78,6 +78,7 @@ public function testRemoveChecksCas() $originalCas = $res->cas(); $this->assertNotNull($originalCas); + $this->fixCavesTimeResolutionOnWindows(); $res = $collection->remove($id, RemoveOptions::build()->cas($originalCas)); $this->assertNotEquals($originalCas, $res->cas()); } diff --git a/tests/KeyValueReplaceTest.php b/tests/KeyValueReplaceTest.php index e4eae905..bdd4a8f1 100644 --- a/tests/KeyValueReplaceTest.php +++ b/tests/KeyValueReplaceTest.php @@ -43,6 +43,7 @@ public function testReplaceCompletesIfDocumentExists() $res = $collection->insert($id, ["answer" => 42]); $originalCas = $res->cas(); + $this->fixCavesTimeResolutionOnWindows(); $res = $collection->replace($id, ["answer" => "foo"]); $replacedCas = $res->cas(); $this->assertNotEquals($originalCas, $replacedCas); diff --git a/tests/KeyValueScanTest.php b/tests/KeyValueScanTest.php index d9351231..1da1a603 100644 --- a/tests/KeyValueScanTest.php +++ b/tests/KeyValueScanTest.php @@ -21,10 +21,14 @@ use Couchbase\CollectionInterface; use Couchbase\Exception\CollectionNotFoundException; use Couchbase\Exception\FeatureNotAvailableException; +use Couchbase\Management\CollectionSpec; +use Couchbase\Management\CollectionManager; use Couchbase\PrefixScan; use Couchbase\RangeScan; use Couchbase\SamplingScan; use Couchbase\ScanOptions; +use Couchbase\UpsertOptions; +use Couchbase\DurabilityLevel; use Couchbase\ScanResults; use Couchbase\ScanTerm; @@ -38,6 +42,8 @@ class KeyValueScanTest extends Helpers\CouchbaseTestCase private array $batchByteLimitValues = [0, 1, 25, 100]; private array $batchItemLimitValues = [0, 1, 25, 100]; private array $concurrencyValues = [1, 2, 4, 8, 32, 128]; + private CollectionManager $manager; + private CollectionSpec $collectionSpec; public function setUp(): void { @@ -45,11 +51,37 @@ public function setUp(): void $this->skipIfProtostellar(); $this->skipIfUnsupported($this->version()->supportsCollections()); - $this->collection = $this->defaultCollection(); + $bucket = $this->openBucket(self::env()->bucketName()); + $defaultScope = $bucket->defaultScope(); + $this->manager = $bucket->collections(); + + $collectionName = $this->uniqueId("collection"); + $this->collectionSpec = new CollectionSpec($collectionName, $defaultScope->name()); + $this->manager->createCollection($this->collectionSpec); + + $seenNewCollection = 0; + + while ($seenNewCollection < 10) { + usleep(10000); + foreach ($this->manager->getAllScopes() as $scope) { + if ($scope->name() == $defaultScope->name()) { + foreach ($scope->collections() as $collection) { + if ($collection->name() == $collectionName) { + $seenNewCollection += 1; + break; + } + } + break; + } + } + } + + $this->collection = $defaultScope->collection($collectionName); + $options = UpsertOptions::build()->durabilityLevel(DurabilityLevel::MAJORITY_AND_PERSIST_TO_ACTIVE); for ($i = 0; $i < 100; $i++) { $s = str_pad((string)$i, 2, "0", STR_PAD_LEFT); $id = $this->sharedPrefix . "-" . $s; - $this->collection->upsert($id, ['num' => $s]); + $this->collection->upsert($id, ['num' => $s], $options); $this->testIds[] = $id; } } @@ -64,6 +96,8 @@ public function tearDown(): void } catch (Exception $exception) { } } + + $this->manager->dropCollection($this->collectionSpec); } public function validateScan(ScanResults $scanResults, array $expectedIds, bool $idsOnly = false) @@ -440,7 +474,6 @@ public function testRangeScanConcurrency() { $this->skipIfCaves(); $this->skipIfUnsupported($this->version()->supportsRangeScan()); - $this->markTestSkipped("Skipped until CXXCBC-345 is resolved"); $expectedIds = range("10", "29"); $expectedIds = array_map( @@ -466,7 +499,6 @@ public function testPrefixScanConcurrency() { $this->skipIfCaves(); $this->skipIfUnsupported($this->version()->supportsRangeScan()); - $this->markTestSkipped("Skipped until CXXCBC-345 is resolved"); $expectedIds = range("10", "19"); $expectedIds = array_map( @@ -489,7 +521,6 @@ public function testSamplingScanConcurrency() { $this->skipIfCaves(); $this->skipIfUnsupported($this->version()->supportsRangeScan()); - $this->markTestSkipped("Skipped until CXXCBC-345 is resolved"); $limit = 20; diff --git a/tests/KeyValueTouchingTest.php b/tests/KeyValueTouchingTest.php index fb31c064..81090c6b 100644 --- a/tests/KeyValueTouchingTest.php +++ b/tests/KeyValueTouchingTest.php @@ -36,7 +36,7 @@ public function testGetAndTouchChangesExpiry() $res = $collection->get($id, GetOptions::build()->withExpiry(true)); $this->assertNull($res->expiryTime()); - $res = $collection->getAndTouch($id, 5); + $res = $collection->getAndTouch($id, 10); $gatCas = $res->cas(); $this->assertNotNull($gatCas); $this->assertNotEquals($originalCas, $gatCas); diff --git a/tests/SearchTest.php b/tests/SearchTest.php index 5c655e83..60467aa3 100644 --- a/tests/SearchTest.php +++ b/tests/SearchTest.php @@ -92,15 +92,31 @@ public function createSearchIndex(int $datasetSize): void $index->setParams($indexDump["params"]); $this->indexManager->upsertIndex($index); + $previousIndexed = 0; + $numberOfDocumentsHasNotChanged = 0; $start = time(); while (true) { try { $indexedDocuments = $this->indexManager->getIndexedDocumentsCount("beer-search"); fprintf(STDERR, "%ds, Indexing 'beer-search': %d docs\n", time() - $start, $indexedDocuments); if ($indexedDocuments >= $datasetSize) { - break; + // the indexer settled on the same number of the documents + // since last check + if ($previousIndexed == $indexedDocuments) { + $numberOfDocumentsHasNotChanged += 1; + } else { + // the indexer still working, just very slow + $numberOfDocumentsHasNotChanged = 0; + } + if ($numberOfDocumentsHasNotChanged > 3) { + // the indexer returns the same number of the indexed + // document three times in a row, maybe it is done + // already? + break; + } } - sleep(5); + $previousIndexed = $indexedDocuments; + sleep(4); } catch (\Couchbase\Exception\IndexNotReadyException $ex) { } } @@ -151,7 +167,7 @@ public function testSearchWithLimit() } } - public function testSearchQueryWithRequestAPI() + public function testSearchQueryWithRequestApi() { $this->skipIfCaves(); @@ -210,8 +226,8 @@ public function testSearchWithConsistency() // Eventual consistency for consistent with... $result = $this->retryFor( - 5, - 50, + 1, + 200, function () use ($query, $options) { $result = $this->cluster->searchQuery("beer-search", $query, $options); if (count($result->rows()) == 0) { @@ -381,16 +397,16 @@ public function testCompoundSearchQueries() $this->skipIfCaves(); $nameQuery = (new MatchSearchQuery("green"))->field("name")->boost(3.4); - $descriptionQuery = (new MatchSearchQuery("hop"))->field("description")->fuzziness(1); + $descriptionQuery = (new MatchSearchQuery("fuggles"))->field("description")->fuzziness(1); $disjunctionQuery = new DisjunctionSearchQuery([$nameQuery, $descriptionQuery]); $options = SearchOptions::build()->fields(["type", "name", "description"]); $result = $this->cluster->searchQuery("beer-search", $disjunctionQuery, $options); - $this->assertGreaterThan(20, $result->metaData()->totalHits()); + $this->assertGreaterThanOrEqual(10, $result->metaData()->totalHits()); $this->assertNotEmpty($result->rows()); $this->assertMatchesRegularExpression('/green/i', $result->rows()[0]['fields']['name']); - $this->assertDoesNotMatchRegularExpression('/hop/i', $result->rows()[0]['fields']['name']); - $this->assertMatchesRegularExpression('/hop/i', $result->rows()[0]['fields']['description']); + $this->assertDoesNotMatchRegularExpression('/fuggles/i', $result->rows()[0]['fields']['name']); + $this->assertMatchesRegularExpression('/fuggles/i', $result->rows()[0]['fields']['description']); $this->assertDoesNotMatchRegularExpression('/green/i', $result->rows()[0]['fields']['description']); $disjunctionQuery->min(2); @@ -462,18 +478,18 @@ public function testSearchWithFacets() $this->assertNotNull($result->facets()['foo']); $this->assertEquals('name', $result->facets()['foo']->field()); $this->assertEquals('ale', $result->facets()['foo']->terms()[0]->term()); - $this->assertGreaterThan(10, $result->facets()['foo']->terms()[0]->count()); + $this->assertGreaterThanOrEqual(10, $result->facets()['foo']->terms()[0]->count()); $this->assertNotNull($result->facets()['bar']); $this->assertEquals('updated', $result->facets()['bar']->field()); $this->assertEquals('old', $result->facets()['bar']->dateRanges()[0]->name()); - $this->assertGreaterThan(30, $result->facets()['bar']->dateRanges()[0]->count()); + $this->assertGreaterThanOrEqual(30, $result->facets()['bar']->dateRanges()[0]->count()); $this->assertNotNull($result->facets()['baz']); $this->assertEquals('abv', $result->facets()['baz']->field()); $this->assertEquals('light', $result->facets()['baz']->numericRanges()[0]->name()); $this->assertGreaterThan(0, $result->facets()['baz']->numericRanges()[0]->max()); - $this->assertGreaterThan(15, $result->facets()['baz']->numericRanges()[0]->count()); + $this->assertGreaterThanOrEqual(15, $result->facets()['baz']->numericRanges()[0]->count()); } public function testNullInNumericRangeFacet() diff --git a/tests/UserManagerTest.php b/tests/UserManagerTest.php index a2d4e6db..b5a25b7b 100644 --- a/tests/UserManagerTest.php +++ b/tests/UserManagerTest.php @@ -29,6 +29,36 @@ public function setUp(): void $this->manager = $this->connectCluster()->users(); } + public function waitForGroupCreated(string $groupName) + { + $seenNewGroup = 0; + + while ($seenNewGroup < 10) { + try { + return $this->manager->getGroup($groupName); + } catch (Exception $ex) { + usleep(100000); + continue; + } + $seenNewGroup += 1; + } + } + + public function waitForUserCreated(string $userName) + { + $seenNewUser = 0; + + while ($seenNewUser < 10) { + try { + return $this->manager->getUser($userName); + } catch (Exception $ex) { + usleep(100000); + continue; + } + $seenNewUser += 1; + } + } + public function testGetRoles() { // Caves doesn't include display name and description for roles. @@ -57,20 +87,19 @@ protected function assertGroup(Group $result, string $groupName, string $desc) $this->assertEquals($groupName, $result->name()); $this->assertEquals($desc, $result->description()); $roles = $result->roles(); - $this->assertCount(4, $roles); - $this->assertGreaterThan(0, count($roles)); + /** @var Role $role */ foreach ($roles as $role) { switch ($role->name()) { case 'bucket_full_access': case 'bucket_admin': - if (!($role->bucket() == 'travel-sample' || $role->bucket() == 'beer-sample')) { - $this->fail(sprintf('wrong bucket for group role $%s', $role->name())); + if ($role->bucket() != 'travel-sample' && $role->bucket() != self::env()->bucketName()) { + $this->fail(sprintf('wrong bucket "%s" for group role "%s"', $role->bucket(), $role->name())); } break; default: - $this->fail(sprintf('unknown group role $%s', $role->name())); + $this->fail(sprintf('unknown group role "%s"', $role->name())); } } } @@ -80,25 +109,22 @@ public function testGroups() $this->skipIfCaves(); $groupName = $this->uniqueId('test'); $desc = 'Users who have full access to sample buckets'; + $defaultBucket = self::env()->bucketName(); $group = Group::build()->setName($groupName)->setDescription($desc)->setRoles( [ Role::build()->setName('bucket_admin')->setBucket('travel-sample'), Role::build()->setName('bucket_full_access')->setBucket('travel-sample'), - Role::build()->setName('bucket_admin')->setBucket('beer-sample'), - Role::build()->setName('bucket_full_access')->setBucket('beer-sample'), + Role::build()->setName('bucket_admin')->setBucket($defaultBucket), + Role::build()->setName('bucket_full_access')->setBucket($defaultBucket), ] ); $this->manager->upsertGroup($group); + $this->waitForGroupCreated($groupName); - $result = $this->retryFor( - 5, - 100, - function () use ($groupName) { - return $this->manager->getGroup($groupName); - } - ); + $result = $this->manager->getGroup($groupName); $this->assertGroup($result, $groupName, $desc); + $this->assertCount(4, $result->roles()); $result = $this->manager->getAllGroups(); $this->assertGreaterThan(0, count($result)); @@ -114,8 +140,14 @@ function () use ($groupName) { $this->assertTrue($found); $this->manager->dropGroup($groupName); + $this->expectException(GroupNotFoundException::class); - $this->manager->getGroup($groupName); + $retry = 10; + while ($retry > 0) { + $this->manager->getGroup($groupName); + $retry -= 1; + usleep(100000); + } } protected function assertUser(UserAndMetadata $result, string $username, string $displayName, Group $group, Role $userRole) @@ -157,6 +189,10 @@ public function testUsers() ); $this->manager->upsertGroup($group); + $this->waitForGroupCreated($groupName); + + $result = $this->manager->getGroup($groupName); + $this->assertGroup($result, $groupName, $desc); $role = Role::build()->setName('bucket_full_access')->setBucket('*'); @@ -165,15 +201,9 @@ public function testUsers() $user = User::build()->setUsername($username)->setPassword('secret')->setDisplayName($display) ->setGroups([$groupName])->setRoles([$role]); $this->manager->upsertUser($user); + $this->waitForUserCreated($username); - $result = $this->retryFor( - 5, - 100, - function () use ($username) { - return $this->manager->getUser($username); - } - ); - + $result = $this->manager->getUser($username); $this->assertUser($result, $username, $display, $group, $role); $result = $this->manager->getAllUsers(); @@ -191,7 +221,12 @@ function () use ($username) { $this->manager->dropUser($username); $this->expectException(UserNotFoundException::class); - $this->manager->getUser($username); + $retry = 10; + while ($retry > 0) { + $this->manager->getUser($username); + $retry -= 1; + usleep(100000); + } } public function testChangePassword() @@ -203,14 +238,7 @@ public function testChangePassword() $display = 'Test User'; $user = User::build()->setUsername($username)->setPassword("secret")->setDisplayName($display)->setRoles([$role]); $this->manager->upsertUser($user); - - $this->retryFor( - 5, - 100, - function () use ($username) { - return $this->manager->getUser($username); - } - ); + $this->waitForUserCreated($username); $options = new ClusterOptions(); $options->credentials($username, "secret"); diff --git a/tests/beer-search.json b/tests/beer-search.json index f666aeff..6db5d94a 100644 --- a/tests/beer-search.json +++ b/tests/beer-search.json @@ -27,7 +27,7 @@ "docvalues_dynamic": true, "index_dynamic": true, "store_dynamic": false, - "type_field": "_type", + "type_field": "type", "types": { "beer": { "dynamic": true,