Add FerretDB v2 (#4689)

This commit is contained in:
Alexey Palazhchenko authored and GitHub committed 2025-01-23 20:29:50 +04:00
1 parent 046d266506
commit e7173c3d8d
714 files changed
+35843 -66453

No files matched your search

+1
View File
@@ -15,3 +15,4 @@
# additional files for Docker images
!build/ferretdb
!build/postgres-documentdb
-17
View File
@@ -2,20 +2,3 @@
# Order is important; the last matching pattern takes the most precedence.
* @AlekSi
/cmd/ @FerretDB/leads
/ferretdb/ @FerretDB/leads
/integration/ @FerretDB/leads
/internal/ @FerretDB/leads
/internal/backends/ @AlekSi
# @AlekSi for engineering, @ptrfarkas for marketing.
# See also Mergify configuration.
/website/ @AlekSi @ptrfarkas
/build/ @AlekSi
/build/legacy-mongo-shell/ @FerretDB/specops
go.mod @AlekSi
go.sum @AlekSi
+7 -16
View File
@@ -14,18 +14,9 @@ body:
- type: textarea
id: version
attributes:
label: FerretDB version
label: FerretDB, PostgreSQL, and DocumentDB versions
description: >
What version and package of FerretDB do you use?
Please provide an output of running FerretDB with [`--version` flag](https://docs.ferretdb.io/v1.24/configuration/flags/).
validations:
required: true
- type: input
id: backend
attributes:
label: Backend
description: >
What backend (PostgreSQL, etc) and what version do you use?
Please add the output of the `serverStatus` command.
validations:
required: true
- type: textarea
@@ -33,9 +24,9 @@ body:
label: Environment
description: |
For example:
- **OS**: Ubuntu 22.04 x86_64
- **Deployment**: Docker using `ghcr.io/ferretdb/ferretdb:1.15.0` image
- **Deployment details**: Docker version 24.0.6
- **OS**: Ubuntu 24.04 x86_64
- **Deployment**: Docker using `ghcr.io/ferretdb/ferretdb:2` image
- **Deployment details**: Docker version 27.3.1
value: |
- OS:
- Deployment:
@@ -48,7 +39,7 @@ body:
label: What did you do?
description: >
Include MongoDB queries or
[pull request with a test script](https://github.com/FerretDB/FerretDB/blob/main-v1/CONTRIBUTING.md#reporting-a-bug)
[pull request with a test script](https://github.com/FerretDB/FerretDB/blob/main/CONTRIBUTING.md#reporting-a-bug)
if you can.
validations:
required: true
@@ -65,6 +56,6 @@ body:
attributes:
label: What did you see instead?
description: >
Please include FerretDB [logs](https://docs.ferretdb.io/v1.24/configuration/observability/).
Please include FerretDB [logs](https://docs.ferretdb.io/configuration/observability/).
validations:
required: true
+3 -3
View File
@@ -36,13 +36,13 @@
4. Check:
- https://hub.docker.com/r/ferretdb/ferretdb/tags
- https://hub.docker.com/r/ferretdb/ferretdb-dev/tags
- https://hub.docker.com/r/ferretdb/all-in-one/tags
- https://hub.docker.com/r/ferretdb/ferretdb-eval/tags
- https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb
- https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb-dev
- https://github.com/FerretDB/FerretDB/pkgs/container/all-in-one
- https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb-eval
- https://quay.io/repository/ferretdb/ferretdb?tab=tags
- https://quay.io/repository/ferretdb/ferretdb-dev?tab=tags
- https://quay.io/repository/ferretdb/all-in-one?tab=tags
- https://quay.io/repository/ferretdb/ferretdb-eval?tab=tags
5. Close milestone in issues.
6. Publish release on GitHub.
7. Announce it on Slack.
+32
View File
@@ -3,6 +3,14 @@
# Apply the "not ready" label to prevent cascaded CI runs on each auto-merge.
version: 2
registries:
ghcr:
type: docker-registry
url: ghcr.io
username: ferretdb-bot
password: ${{ secrets.PRIVATE_DEPENDABOT_TOKEN }}
updates:
- package-ecosystem: "github-actions"
directory: "/"
@@ -18,6 +26,7 @@ updates:
time: "01:42"
- package-ecosystem: "docker"
registries: ["ghcr"]
directory: "/build/ferretdb"
labels: ["deps", "not ready"]
assignees: [AlekSi]
@@ -30,6 +39,19 @@ updates:
day: "wednesday"
time: "01:42"
- package-ecosystem: "docker"
directory: "/build/postgres-documentdb"
labels: ["deps", "not ready"]
assignees: [AlekSi]
open-pull-requests-limit: 20
groups:
build-docker:
patterns: ["*"]
schedule:
interval: "weekly"
day: "wednesday"
time: "01:42"
- package-ecosystem: "docker"
registries: ["ghcr"]
directory: "/build/deps"
labels: ["deps", "not ready"]
assignees: [AlekSi]
@@ -90,3 +112,13 @@ updates:
interval: "weekly"
day: "wednesday"
time: "01:42"
- package-ecosystem: "gitsubmodule"
directory: "/"
labels: ["deps", "not ready"]
assignees: [AlekSi]
open-pull-requests-limit: 20
schedule:
interval: "weekly"
day: "wednesday"
time: "01:42"
+5 -22
View File
@@ -32,6 +32,9 @@ labels:
- name: area/diag
color: "#909BA6"
description: Issues about diagnostic commands
- name: area/errors
color: "#DE231F"
description: Issues about MongoDB-compatible error codes and messages
- name: area/fuzz
color: "#D4C5F9"
description: Issues about fuzzing and smithing
@@ -41,32 +44,12 @@ labels:
- name: area/observability
color: "#E04D33"
description: Issues about metrics, logs, traces, pprof, etc
- name: area/oplog
color: "#09D521"
description: Issues about OpLog and capped collections
- name: area/params
color: "#FBCA04"
description: Issues about command parameters extraction
- name: area/performance
color: "#D02FE8"
description: Issues about performance
- name: area/pushdown
color: "#83F7BA"
description: Issues about query pushdown
- name: area/types
- name: area/wire
color: "#0354F4"
description: Issues about data types, BSON, wire protocol, etc
# backend
- name: backend/pg
color: "#305C83"
description: Issues about PostgreSQL backend
- name: backend/sqlite
color: "#2C8FCA"
description: Issues about SQLite backend
- name: backend/hana
color: "#2D6AB1"
description: Issues about SAP HANA backend
description: Issues about BSON and the wire protocol
# blog
- name: blog/engineering
+35 -43
View File
@@ -13,23 +13,21 @@ on:
required: true
type: number
coveralls:
required: true
type: boolean
disable_pushdown:
required: false
type: boolean
default: false
postgres_dockerfile:
test_timeout:
required: false
type: string
default: "postgres"
default: 20m
no_xfail:
required: false
type: boolean
default: false
checkout_ref:
required: false
type: string
default: ""
secrets:
FERRETDB_HANA_URL:
required: false
env:
GOPATH: /home/runner/go
@@ -51,11 +49,6 @@ jobs:
timeout-minutes: 25
steps:
- name: Dump github context
run: echo "$GITHUB_CONTEXT"
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
@@ -80,10 +73,15 @@ jobs:
run: go generate -x
working-directory: tools
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Start environment
run: bin/task env-up-detach
env:
POSTGRES_DOCKERFILE: ${{ inputs.postgres_dockerfile }}
- name: Run init
run: bin/task init
@@ -93,42 +91,34 @@ jobs:
- name: >
Run ${{ inputs.task }} tests
(${{ inputs.shard_index }}/${{ inputs.shard_total }},
pushdown=${{ !inputs.disable_pushdown }})
(${{ inputs.shard_index }}/${{ inputs.shard_total }})
run: >
bin/task test-integration-${{ inputs.task }}
SHARD_INDEX=${{ inputs.shard_index }}
SHARD_TOTAL=${{ inputs.shard_total }}
TEST_TIMEOUT=20m
DISABLE_PUSHDOWN=${{ inputs.disable_pushdown }}
TEST_TIMEOUT=${{ inputs.test_timeout }}
NO_XFAIL=${{ inputs.no_xfail }}
env:
GOFLAGS: ${{ runner.debug == '1' && '-v' || '' }}
FERRETDB_HANA_URL: ${{ secrets.FERRETDB_HANA_URL }}
ENVTOOL_DEBUG: ${{ runner.debug == '1' && 'true' || 'false' }}
# The token is not required but should make uploads more stable.
# If secrets are unavailable (for example, for a pull request from a fork), it fallbacks to the tokenless uploads.
#
# Unfortunately, it seems that tokenless uploads fail too often.
# See https://github.com/codecov/codecov-action/issues/837.
#
# We also can't use ${{ vars.CODECOV_TOKEN }}: https://github.com/orgs/community/discussions/44322
- name: Upload coverage information to codecov
if: always()
uses: codecov/codecov-action@v4
with:
token: 3c5cbd1b-5110-4302-ad5a-080390ca731a
files: ./integration/integration-${{ inputs.task }}.txt
flags: integration,${{ inputs.task }}-${{ inputs.shard_index }},filter-${{ !inputs.disable_pushdown }}
fail_ci_if_error: true
verbose: true
- name: Collect logs
if: failure()
run: |
mkdir -p /tmp/logs
bin/task env-logs-collect > /tmp/logs/compose-logs.txt
cp -v integration/*.json.tmp integration/*.txt.tmp /tmp/logs/
- name: Upload coverage information to coveralls
if: always() && inputs.coveralls
uses: coverallsapp/github-action@v2
- name: Compress logs before upload
if: failure()
run: zip -r -q -9 logs.zip /tmp/logs
- name: Upload logs
if: failure()
uses: actions/upload-artifact@v4
with:
file: ./integration/integration-${{ inputs.task }}.txt
flag-name: integration-${{ inputs.task }}-${{ inputs.shard_index }}-filter-${{ !inputs.disable_pushdown }}
parallel: true
name: logs-${{ inputs.task }}-${{ inputs.shard_index }}-${{ inputs.shard_total }}
path: logs.zip
retention-days: 3
# we don't want them on CI
- name: Clean test and fuzz caches
@@ -136,6 +126,8 @@ jobs:
run: go clean -testcache -fuzzcache
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
+5 -1
View File
@@ -63,7 +63,11 @@ env:
jobs:
conform-pr:
name: Conform PR
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 5
# No `trust` label check because we don't checkout PR's code.
+8 -2
View File
@@ -28,7 +28,11 @@ concurrency:
jobs:
contexts:
name: Contexts
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 5
# comment out to enable this job
@@ -43,7 +47,9 @@ jobs:
lfs: false # LFS is used only by website
- name: Debug git status
run: git status
run: |
git status --untracked-files --ignored
git status
- name: Dump github context
run: echo "$GITHUB_CONTEXT"
+17 -2
View File
@@ -32,7 +32,11 @@ env:
jobs:
build:
name: Build docs
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
@@ -49,6 +53,7 @@ jobs:
# See https://github.com/ppremk/lfs-warning/issues/147.
# But the real issue will be right above that message: a list of files that should be added using Git LFS.
- name: Check LFS status
if: false # disabled until release
uses: ppremk/lfs-warning@v3.3
with:
filesizelimit: 102400b
@@ -67,6 +72,13 @@ jobs:
run: go generate -x
working-directory: tools
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build tools
run: bin/task env-pull
@@ -96,7 +108,9 @@ jobs:
if-no-files-found: error
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
@@ -107,7 +121,8 @@ jobs:
# exclude pull_request and schedule
needs: build
if: github.event_name == 'push'
# if: github.event_name == 'push'
if: false # disabled until release
permissions:
pages: write
+7 -1
View File
@@ -31,7 +31,11 @@ env:
jobs:
format:
name: Format files
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
@@ -60,6 +64,8 @@ jobs:
run: bin/task fmt-yaml
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
-62
View File
@@ -1,62 +0,0 @@
---
# Integration tests for extra configuration.
name: Go Extra
on:
schedule:
- cron: "12 1 * * *" # after Go workflow to reuse "Setup Go" cache
env:
GOPATH: /home/runner/go
GOCACHE: /home/runner/go/cache
GOLANGCI_LINT_CACHE: /home/runner/go/cache/lint
GOMODCACHE: /home/runner/go/mod
GOPROXY: https://proxy.golang.org
GOTOOLCHAIN: local
jobs:
integration:
# job name must be unique; make it unique and nice
name: >
${{ matrix.task }} ${{ matrix.postgres_dockerfile }} ${{ matrix.shard_index }}/${{ matrix.shard_total }}
(pushdown=${{ matrix.disable_pushdown }})
# To avoid conflict with go.yml.
concurrency:
group: ${{ github.workflow }}-integration-${{ matrix.task }}-${{ matrix.postgres_dockerfile }}-${{ matrix.shard_index }}-${{ matrix.disable_pushdown }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
task: [postgresql, sqlite, mysql]
shard_index: [1, 2, 3]
shard_total: [3]
disable_pushdown: [false, true]
include:
- task: postgresql
shard_index: 1
shard_total: 3
disable_pushdown: false
postgres_dockerfile: citus
- task: postgresql
shard_index: 2
shard_total: 3
disable_pushdown: false
postgres_dockerfile: citus
- task: postgresql
shard_index: 3
shard_total: 3
disable_pushdown: false
postgres_dockerfile: citus
# Do not submit to coveralls because it can't handle parallel workflows:
# https://github.com/lemurheavy/coveralls-public/issues/1636#issuecomment-1529460515
uses: ./.github/workflows/_integration.yml
with:
task: ${{ matrix.task }}
shard_index: ${{ matrix.shard_index }}
shard_total: ${{ matrix.shard_total }}
disable_pushdown: ${{ matrix.disable_pushdown }}
postgres_dockerfile: ${{ matrix.postgres_dockerfile }}
coveralls: false
-64
View File
@@ -1,64 +0,0 @@
---
# Integration tests that need access to secrets.
name: Go Trusted
on:
pull_request_target:
types:
# not for "labeled" to prevent two builds for "labeled" and "unlabeled" when labels are changed
- unlabeled # if GitHub Actions stuck, add and remove "not ready" label to force rebuild
- opened
- reopened
- synchronize
push:
branches:
- main
- main-*
- releases/*
schedule:
- cron: "12 1 * * *" # after Go workflow to reuse "Setup Go" cache
env:
GOPATH: /home/runner/go
GOCACHE: /home/runner/go/cache
GOLANGCI_LINT_CACHE: /home/runner/go/cache/lint
GOMODCACHE: /home/runner/go/mod
GOPROXY: https://proxy.golang.org
GOTOOLCHAIN: local
jobs:
integration:
# job name must be unique; make it unique and nice
name: ${{ matrix.task }} ${{ matrix.shard_index }}/${{ matrix.shard_total }}
# Do not run this job in parallel for any PR change or branch push
# to save some resources.
concurrency:
group: ${{ github.workflow }}-integration-${{ matrix.task }}-${{ matrix.shard_index }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
if: >
github.event_name != 'pull_request_target' ||
(
contains(github.event.pull_request.labels.*.name, 'trust') &&
!contains(github.event.pull_request.labels.*.name, 'not ready')
)
strategy:
fail-fast: false
matrix:
task: [hana]
shard_index: [1]
shard_total: [1]
# Do not submit to coveralls because it can't handle parallel workflows:
# https://github.com/lemurheavy/coveralls-public/issues/1636#issuecomment-1529460515
uses: ./.github/workflows/_integration.yml
with:
task: ${{ matrix.task }}
shard_index: ${{ matrix.shard_index }}
shard_total: ${{ matrix.shard_total }}
checkout_ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || ''}}
coveralls: false
secrets:
FERRETDB_HANA_URL: ${{ secrets.FERRETDB_HANA_URL }}
+76 -38
View File
@@ -26,7 +26,11 @@ env:
jobs:
short-test:
name: Short test
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 15
# Do not run this job in parallel for any PR change or branch push.
@@ -68,13 +72,19 @@ jobs:
run: go clean -testcache -fuzzcache
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
test:
name: Test
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 15
# Do not run this job in parallel for any PR change or branch push.
@@ -82,7 +92,9 @@ jobs:
group: ${{ github.workflow }}-test-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
# skip this job for now
if: false
# if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
@@ -101,58 +113,45 @@ jobs:
run: go generate -x
working-directory: tools
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Start environment
run: bin/task env-up-detach
- name: Run init
run: bin/task init
- name: Wait for and setup environment
run: bin/task env-setup
- name: Regenerate and reformat
run: bin/task gen
env:
DOCUMENTDB_GEN_URL: postgres://username:password@127.0.0.1:5432/postgres
- name: Build bin/ferretdb
run: bin/task build-host
- name: Wait for and setup environment
run: bin/task env-setup
- name: Run unit tests
run: bin/task test-unit TEST_TIMEOUT=10m
env:
GOFLAGS: ${{ runner.debug == '1' && '-v' || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for tools
# The token is not required but should make uploads more stable.
# If secrets are unavailable (for example, for a pull request from a fork), it fallbacks to the tokenless uploads.
#
# Unfortunately, it seems that tokenless uploads fail too often.
# See https://github.com/codecov/codecov-action/issues/837.
#
# We also can't use ${{ vars.CODECOV_TOKEN }}: https://github.com/orgs/community/discussions/44322
- name: Upload coverage information to codecov
if: always()
uses: codecov/codecov-action@v4
with:
token: 3c5cbd1b-5110-4302-ad5a-080390ca731a
files: ./cover.txt
flags: unit
fail_ci_if_error: true
verbose: true
- name: Upload coverage information to coveralls
uses: coverallsapp/github-action@v2
with:
file: ./cover.txt
flag-name: unit
parallel: true
# we don't want them on CI
- name: Clean test and fuzz caches
if: always()
run: go clean -testcache -fuzzcache
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
@@ -171,7 +170,7 @@ jobs:
strategy:
fail-fast: false
matrix:
task: [postgresql, sqlite]
task: [postgresql]
shard_index: [1, 2, 3, 4, 5]
shard_total: [5]
include:
@@ -184,23 +183,47 @@ jobs:
shard_total: ${{ matrix.shard_total }}
coveralls: true
integration-no-xfail:
name: postgresql no-xfail
# Do not run this job in parallel for any PR change or branch push
# to save some resources.
concurrency:
group: ${{ github.workflow }}-integration-no-xfail-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
uses: ./.github/workflows/_integration.yml
with:
task: postgresql
shard_index: 1
shard_total: 1
test_timeout: 5m
no_xfail: true
# https://github.com/lemurheavy/coveralls-public/issues/1636
submit-coveralls:
name: Submit final coveralls report
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
needs: [test, integration]
if: always() && (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready'))
steps:
- name: Submit coveralls
uses: coverallsapp/github-action@v2
with:
parallel-finished: true
- name: Do nothing
run: echo 'nothing'
env-data:
name: env-data
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
# Do not run this job in parallel for any PR change or branch push.
@@ -227,6 +250,13 @@ jobs:
run: go generate -x
working-directory: tools
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Start environment
run: bin/task env-up-detach
@@ -249,7 +279,11 @@ jobs:
golangci-lint:
name: golangci-lint
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
# Do not run this job in parallel for any PR change or branch push.
@@ -259,6 +293,10 @@ jobs:
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
permissions:
contents: read
issues: read
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
+7 -16
View File
@@ -68,6 +68,7 @@ jobs:
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
ssh-key: ${{ secrets.FERRETDB_DOCUMENTDB_DEPLOY_KEY }}
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout pull request code
@@ -77,6 +78,7 @@ jobs:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
ref: ${{ github.event.pull_request.head.sha }}
ssh-key: ${{ secrets.FERRETDB_DOCUMENTDB_DEPLOY_KEY }}
# for version.txt on push tags; see https://github.com/actions/checkout/issues/290
- name: Fetch annotated tags
@@ -116,12 +118,6 @@ jobs:
id: tag
run: bin/definedockertag
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ferretdbbot
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -129,16 +125,9 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Quay.io
uses: docker/login-action@v3
with:
registry: quay.io
username: ferretdbbot
password: ${{ secrets.QUAY_TOKEN }}
- name: Build and push all-in-one Docker images ${{ steps.tag.outputs.all_in_one_images }}
if: steps.tag.outputs.all_in_one_images != ''
run: bin/task docker-all-in-one-push DOCKER_IMAGES=${{ steps.tag.outputs.all_in_one_images }}
- name: Build and push evaluation Docker images ${{ steps.tag.outputs.evaluation_images }}
if: steps.tag.outputs.evaluation_images != ''
run: bin/task docker-evaluation-push DOCKER_IMAGES=${{ steps.tag.outputs.evaluation_images }}
- name: Build and push development Docker images ${{ steps.tag.outputs.development_images }}
if: steps.tag.outputs.development_images != ''
@@ -190,6 +179,8 @@ jobs:
if-no-files-found: error
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
+5 -1
View File
@@ -31,7 +31,11 @@ env:
jobs:
security-scan:
name: Security scan
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
@@ -1,5 +1,5 @@
---
name: JS
name: Test scripts
on:
pull_request:
types:
@@ -9,6 +9,7 @@ on:
- synchronize
paths:
- build/legacy-mongo-shell/*.js
- build/testscripts/*.sql
push:
branches:
- main
@@ -31,9 +32,13 @@ env:
GOTOOLCHAIN: local
jobs:
testjs:
name: Test script
runs-on: ubuntu-22.04
run:
name: Run test scripts
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')
@@ -49,12 +54,19 @@ jobs:
- name: Setup Go
uses: FerretDB/github-actions/setup-go@main
with:
cache-key: testjs
cache-key: testscripts
- name: Install Task
run: go generate -x
working-directory: tools
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Start environment
run: bin/task env-up-detach
@@ -67,20 +79,26 @@ jobs:
- name: Wait for and setup environment
run: bin/task env-setup
- name: Start FerretDB in the background
- name: Start FerretDB (no auth) in the background
run: >
bin/ferretdb
--listen-addr=:27017
--proxy-addr=127.0.0.1:47017
--proxy-addr=127.0.0.1:37017
--no-auth
--mode=diff-normal
--handler=pg
--postgresql-url='postgres://username@127.0.0.1:5432/ferretdb'
--postgresql-url='postgres://username:password@127.0.0.1:5432/postgres'
2> ferretdb.log &
- name: Run testjs for MongoDB
run: bin/task testjs TESTJS_PORT=47017
- name: Run testsql for PostgreSQL
if: always()
run: bin/task testsql
- name: Run testjs for FerretDB
- name: Run testjs for MongoDB (no auth)
if: always()
run: bin/task testjs TESTJS_PORT=37017
- name: Run testjs for FerretDB (no auth)
if: always()
run: bin/task testjs
- name: Compress FerretDB log before upload
@@ -108,6 +126,8 @@ jobs:
run: go clean -testcache -fuzzcache
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
+14 -2
View File
@@ -26,7 +26,11 @@ env:
jobs:
blog-preview:
name: Preview blog
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
if: >
@@ -77,7 +81,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for checkdocs
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
@@ -93,7 +99,11 @@ jobs:
docs-preview:
name: Preview docs
runs-on: ubuntu-22.04
# https://www.ubicloud.com/docs/about/pricing#github-actions
# https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-larger-runners
runs-on: ubicloud-standard-2
timeout-minutes: 10
if: >
@@ -148,7 +158,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for checkdocs
- name: Check dirty
if: always()
run: |
git status --untracked-files --ignored
git status
git diff --exit-code
+4 -2
View File
@@ -1,12 +1,14 @@
# Please use global .gitignore file for .vscode/, .idea/, go.work, .envrc, etc.
# This file is used only for repository-specific patterns.
# Please use global .gitignore file for .DS_Store, .idea/, go.work, .envrc, etc.
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer
docker-compose.override.yml
state.json
telemetry.json
*.out
*.sqlite*
*.tmp
*.txt
!website/static/*.txt
+4
View File
@@ -0,0 +1,4 @@
[submodule "documentdb"]
path = build/postgres-documentdb/documentdb
url = https://github.com/FerretDB/documentdb.git
branch = main
+6 -11
View File
@@ -5,6 +5,7 @@
run:
timeout: 3m
# bin/golangci-lint linters -c .golangci-new.yml
linters-settings:
asasalint:
exclude:
@@ -18,8 +19,7 @@ linters-settings:
check-blank: false
disable-default-exclusions: false
exclude-functions:
- (*database/sql.Rows).Close
- (*github.com/FerretDB/FerretDB/internal/util/fsql.Rows).Close
- (*github.com/jackc/pgx/v5.Conn).Close
errorlint:
# see caveats at https://github.com/polyfloyd/go-errorlint#fmterrorf-wrapping-verb
errorf: false
@@ -41,14 +41,6 @@ linters-settings:
- name: package-comments
staticcheck:
checks: ["all"]
tagalign:
sort: true
order:
- name
- cmd
- arg
- default
- help
wsl:
# https://github.com/bombsimon/wsl/blob/master/doc/configuration.md
strict-append: false
@@ -86,6 +78,8 @@ linters:
- misspell
- nolintlint
- sloglint
- tagalign
- unparam
- unused
- whitespace
@@ -123,6 +117,7 @@ linters:
- gosec
- gosmopolitan
- grouper
- iface
- inamedparam
- interfacebloat
- intrange
@@ -147,6 +142,7 @@ linters:
- promlinter
- protogetter
- reassign
- recvcheck
- rowserrcheck
- sloglint
- spancheck
@@ -160,7 +156,6 @@ linters:
- tparallel
- typecheck
- unconvert
- unparam
- usestdlibvars
- varnamelen
- wastedassign
+63 -52
View File
@@ -16,8 +16,8 @@ linters-settings:
desc: use `context` package instead
- pkg: golang.org/x/exp/slices
desc: use `slices` package instead
- pkg: maps
desc: use `golang.org/x/exp/maps` package instead
# - pkg: golang.org/x/exp/maps
# desc: use `maps` package instead
- pkg: github.com/jackc/pgconn
desc: use `github.com/jackc/pgx/v5/pgconn` package instead
- pkg: github.com/jackc/pgproto3
@@ -28,48 +28,42 @@ linters-settings:
desc: use `github.com/jackc/pgx/v5` package instead
- pkg: github.com/jackc/pgx/v4
desc: use `github.com/jackc/pgx/v5` package instead
- pkg: github.com/uber-go/zap
desc: use `log/slog` package instead
#- pkg: github.com/xdg-go/scram
# desc: use `github.com/FerretDB/FerretDB/v2/internal/util/scram` package instead
ferretdb-in-tools:
files:
- "**/tools/**/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/
- pkg: github.com/FerretDB/FerretDB/v2/internal/
desc: tools should not import FerretDB code
types:
files:
- "**/internal/util/logging/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/types
sjson:
files:
- $all
- "!**/internal/backends/sqlite/*.go"
- "!**/internal/backends/postgresql/*.go"
- "!**/internal/backends/postgresql/metadata/*.go"
- "!**/internal/backends/mysql/*.go"
- "!**/internal/backends/mysql/metadata/*.go"
- "!**/internal/backends/hana/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/handler/sjson
common:
files:
- "**/internal/backends/**.go"
- "**/internal/clientconn/cursor/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/handler/common
handlererrors:
wirebson:
files:
- $all
- "!**/cmd/envtool/*.go"
- "!**/internal/clientconn/*.go"
- "!**/internal/dataapi/server/*.go"
- "!**/internal/documentdb/**.go"
- "!**/internal/driver/*.go"
- "!**/internal/handler/**.go"
- "!**/internal/mongoerrors/*.go"
- "!**/internal/util/logging/*.go"
- "!**/internal/util/scram/*.go"
- "!**/internal/util/testutil/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/handler/handlererrors
backends:
- pkg: github.com/FerretDB/wire/wirebson
documentdb:
files:
- "**/internal/clientconn/cursor/*.go"
- "**/internal/documentdb/genwrap/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/backends
- pkg: github.com/FerretDB/FerretDB/v2/internal/documentdb
cursor:
files:
- $all
- "!**/internal/documentdb/**.go"
deny:
- pkg: github.com/FerretDB/FerretDB/v2/internal/documentdb/cursor
exhaustive:
default-signifies-exhaustive: false
forbidigo:
@@ -81,35 +75,35 @@ linters-settings:
# integration tests use a different configuration file
- p: ^\Qbson.E\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson\E$
msg: Use `*types.Document` or `*types.Array` instead.
msg: Use `*bson.Document` or `*bson.Array` instead.
- p: ^\Qprimitive.E\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson/primitive\E$
msg: Use `*types.Document` or `*types.Array` instead.
msg: Use `*bson.Document` or `*bson.Array` instead.
- p: ^\Qbson.D\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson\E$
msg: Use `*types.Document` instead.
msg: Use `*bson.Document` instead.
- p: ^\Qprimitive.D\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson/primitive\E$
msg: Use `*types.Document` instead.
msg: Use `*bson.Document` instead.
- p: ^\Qbson.M\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson\E$
msg: Use `*types.Document` instead.
msg: Use `*bson.Document` instead.
- p: ^\Qprimitive.M\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson/primitive\E$
msg: Use `*types.Document` instead.
msg: Use `*bson.Document` instead.
- p: ^\Qbson.A\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson\E$
msg: Use `*types.Array` instead.
msg: Use `*bson.Array` instead.
- p: ^\Qprimitive.A\E$
pkg: ^\Qgo.mongodb.org/mongo-driver/bson/primitive\E$
msg: Use `*types.Array` instead.
msg: Use `*bson.Array` instead.
exclude-godoc-examples: true
analyze-types: true
gci:
sections:
- standard
- default
- prefix(github.com/FerretDB/FerretDB)
- prefix(github.com/FerretDB/FerretDB) # both /v2 and /tools
goconst:
min-occurrences: 5
ignore-tests: true
@@ -123,6 +117,8 @@ linters-settings:
- BUG
- FIXME
- HACK
- lala
- lalala
goheader:
# Please do not update it, including bumping a year.
template: |-
@@ -140,10 +136,12 @@ linters-settings:
See the License for the specific language governing permissions and
limitations under the License.
goimports:
local-prefixes: github.com/FerretDB/FerretDB
local-prefixes: github.com/FerretDB/FerretDB/v2
gomodguard:
blocked:
modules: []
modules:
- github.com/FerretDB/FerretDB:
reason: "Use `github.com/FerretDB/FerretDB/v2/...` instead."
gosimple:
checks: ["all"]
govet:
@@ -153,15 +151,12 @@ linters-settings:
- nilness
- shadow
importas:
no-unaliased: false
no-extra-aliases: true
# https://github.com/julz/importas/issues/15
no-unaliased: true
no-extra-aliases: false
alias:
- pkg: github.com/prometheus/client_model/go
alias: dto
- pkg: modernc.org/sqlite
alias: sqlite3
- pkg: modernc.org/sqlite/lib
alias: sqlite3lib
- pkg: go.opentelemetry.io/otel/attribute
alias: otelattribute
- pkg: go.opentelemetry.io/otel/codes
@@ -174,8 +169,6 @@ linters-settings:
alias: otelsdktrace
- pkg: go.opentelemetry.io/otel/semconv/v1.21.0
alias: otelsemconv
- pkg: github.com/FerretDB/FerretDB/internal/backends/mysql/metadata/pool
alias: mysqlpool
# ineffassign
lll:
line-length: 130
@@ -211,7 +204,23 @@ linters-settings:
checks:
- all
- -SA1019 # ignore deprecation errors in existing code; new code is checked by the other configuration
# unused
tagalign:
sort: true
order:
- name
- cmd
- arg
- default
- help
unparam:
check-exported: false
unused:
field-writes-are-uses: false
post-statements-are-reads: false
exported-fields-are-used: false
parameters-are-used: true # it should be false, TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/823
local-variables-are-used: false
generated-is-used: false
whitespace:
multi-if: false
multi-func: false
@@ -241,6 +250,8 @@ linters:
- revive
- sloglint
- staticcheck
- tagalign
- unparam
- unused
- whitespace
+2
View File
@@ -1,3 +1,5 @@
# We can't have it in build/.prettierignore due to https://github.com/prettier/prettier/issues/15428
# Textlint uses the same format, so we re-use the file.
build/postgres-documentdb/documentdb/**
internal/dataapi/api/openapi.json
website/static/codapi/snippet.js
+3 -1704
View File
File diff suppressed because it is too large. Load diff
+65 -47
View File
@@ -30,7 +30,7 @@ On macOS and Windows, [Docker Desktop](https://www.docker.com/products/docker-de
On Windows, it should be [configured to use WSL 2](https://docs.docker.com/desktop/windows/wsl/) without any distro;
all commands should be run on the host.
You will need Go 1.22 or later on the host.
You will need Go 1.23 or later on the host.
If your package manager doesn't provide it yet,
please install it from [go.dev](https://go.dev/dl/).
@@ -106,8 +106,6 @@ To help us accurately identify the cause, we encourage you to include a pull req
Please write the test script in [build/legacy-mongo-shell/test.js](build/legacy-mongo-shell/test.js).
You can find an overview of the available assertions [here](build/legacy-mongo-shell/README.md).
Use these assertions to validate your test's assumptions and invariants.
You can also find an example of how to prepare a test script in
[build/legacy-mongo-shell/test.example.js](build/legacy-mongo-shell/test.example.js).
With `task` installed (see above), you may test your script using following steps:
@@ -125,12 +123,20 @@ With `task` installed (see above), you may do the following:
1. Start the development environment with `task env-up`.
2. Run all tests with `task test` (see [below](#running-tests)).
3. Start FerretDB with `task run`, `task run-sqlite` or `task run-proxy`.
(See [Operation modes](https://docs.ferretdb.io/v1.24/configuration/operation-modes/) page in our documentation.)
3. Start FerretDB with `task run`.
The development environment uses `diff-normal` mode as the default mode.
Set preferred mode such as `task run MODE=diff-proxy`.
(See [Operation modes](https://docs.ferretdb.io/configuration/operation-modes/) page in our documentation.)
4. Fill collections in the `test` database with data for experiments with `task env-data`.
5. Run `mongosh` with `task mongosh`.
See what collections were created by the previous command with `show collections`.
#### Commands for authenticated connection
1. Start the development environment with `task env-up`.
2. Start FerretDB with `task run-secure MODE=normal` or `task run-secure MODE=proxy`.
3. Run `task mongosh-secure` to establish an authenticated connection.
### Code overview
The directory `cmd` provides commands implementation.
@@ -141,19 +147,19 @@ They are installed into `bin/` by `cd tools; go generate -x`.
The `internal` subpackages contain most of the FerretDB code:
- `types` package provides Go types matching BSON types that don't have built-in Go equivalents:
we use `int32` for BSON's int32, but `types.ObjectID` for BSON's ObjectId.
- `bson` package provides converters from/to BSON for built-in and `types` types.
- `wire` package provides wire protocol implementation.
- `clientconn` package provides client connection implementation.
It accepts client connections, reads `wire`/`bson` protocol messages, and passes them to `handler`.
Responses are then converted to `wire`/`bson` messages and sent back to the client.
- `handler` contains implementations of command handlers.
They use `types` and `wire` packages, but `bson` package details are hidden.
- `handler/sjson` provides converters from/to SJSON for built-in and `types` types.
SJSON adds some extensions to JSON for keeping object keys in order,
preserving BSON type information in the values themselves, etc.
It is used by some backends.
- `handler` provides implementations of command handlers.
The logic of commands like `find` and `aggregate` is there.
- `dataapi` provides an Data API wrapper for `handler`.
It allows FerretDB to be used over HTTP instead of MongoDB wire protocol.
- `clientconn` provides wire protocol server implementation.
It accepts client connections, reads wire protocol messages, and passes them to `handler`.
Responses are then converted and sent back to the client.
- `documentdb` provides DocumentDB extension integration.
- `mongoerrors` provides MongoDB-compatible error types and codes.
They are used by `handler` and `documentdb`.
The wire protocol implementation and BSON handling code were extracted into a separate repository:
https://github.com/FerretDB/wire
#### Running tests
@@ -169,13 +175,13 @@ They use the Go MongoDB driver like a regular user application.
They could test any MongoDB-compatible database (such as FerretDB or MongoDB itself) via a regular TCP or TLS port
or Unix domain socket.
They also could test in-process FerretDB instances
(meaning that integration tests start and stop them themselves) with a given backend.
(meaning that integration tests start and stop them themselves).
Finally, some integration tests (so-called compatibility or "compat" tests) connect to two systems
("target" for FerretDB and "compat" for MongoDB) at the same time,
send the same queries to both, and compare results.
You can run them with:
- `task test-integration-postgresql` for in-process FerretDB with PostgreSQL backend and MongoDB;
- `task test-integration-postgresql` for in-process FerretDB and MongoDB;
- `task test-integration-mongodb` for MongoDB only, skipping compat tests;
- or `task test-integration` to run all in parallel.
@@ -185,7 +191,7 @@ If tests fail and the output is too confusing, try running them sequentially by
You can also run `task -C 1` to limit the number of concurrent tasks, which is useful for debugging.
To run a subset of integration tests and test cases, you may use Task variable `TEST_RUN`.
For example, to run all tests related to the `getMore` command implementation for in-process FerretDB with PostgreSQL backend
For example, to run all tests for the `getMore` command with in-process FerretDB
you may use `task test-integration-postgresql TEST_RUN='(?i)GetMore'`.
Finally, since all tests just run `go test` with various arguments and flags under the hood
@@ -194,12 +200,10 @@ you may also use all standard `go` tool facilities,
including [`GOFLAGS` environment variable](https://pkg.go.dev/cmd/go#hdr-Environment_variables).
For example:
- to run all tests related to the `getMore` command implementation in-process FerretDB with PostgreSQL backend
with all subtests running sequentially,
you may use `env GOFLAGS='-parallel=1' task test-integration-postgresql TEST_RUN='(?i)GetMore'`;
- to run all tests for in-process FerretDB with SQLite backend
with [Go execution tracer](https://pkg.go.dev/runtime/trace) enabled,
you may use `env GOFLAGS='-trace=trace.out' task test-integration-sqlite`.
- to run all tests for the `getMore` command sequentially,
you may use `env GOFLAGS='-parallel=1 -p=1' task test-integration-postgresql TEST_RUN='(?i)GetMore'`;
- to run all tests with MongoDB with [Go execution tracer](https://pkg.go.dev/runtime/trace) enabled
you may use `env GOFLAGS='-trace=trace.out' task test-integration-mongodb`.
<!-- textlint-disable one-sentence-per-line -->
@@ -239,10 +243,10 @@ and some other pages such as [Spelling](https://go.dev/wiki/Spelling).
Some of our idiosyncrasies are documented below.
1. We use type switches over BSON types in many places in our code.
The order of `case`s follows this order: https://pkg.go.dev/github.com/FerretDB/FerretDB/internal/types#hdr-Mapping
The order of `case`s follows this order: https://pkg.go.dev/github.com/FerretDB/wire/wirebson#hdr-Types
It may seem random, but it is only pseudo-random and follows BSON spec: https://bsonspec.org/spec.html
2. We generally pass and return `struct`s by pointers.
There are some exceptions like `types.Path` that have value semantics, but when in doubt – use pointers.
There are some exceptions like `time.Time` that have value semantics, but when in doubt – use pointers.
#### Comments conventions
@@ -271,21 +275,23 @@ Some of our idiosyncrasies are documented below.
#### Integration tests conventions
We prefer our integration tests to be straightforward,
branchless (with a few, if any, `if` and `switch` statements),
and backend-independent.
Ideally, the same test should work for both FerretDB with all backends and MongoDB.
We prefer our integration tests to be straightforward and
branchless (with a few, if any, `if` and `switch` statements).
Ideally, the same test should work for both FerretDB and MongoDB.
If that's impossible without some branching, use helpers exported from the `setup` package,
such us `FailsForFerretDB`, `SkipForMongoDB`, etc.
Please use expected failure rather than skipping the test, whenever it's possible.
It makes tracking development progress much easier.
The bar for using other ways of branching, such as checking error codes and messages, is very high.
Writing separate tests might be much better than making a single test that checks error text.
Also, we should use driver methods as much as possible instead of testing commands directly via `RunCommand`.
We have a lot of existing helpers to convert the driver's `bson.D` values to our `*types.Document` values,
to compare them, etc.
In most cases, they should be used instead of (deprecated) `bson.D.Map()`,
`bson.Unmarshal`, decoding into a struct with `bson` tags, comparing fields one-by-one, etc.
In most cases, helpers such as `AssertEqualDocuments` are used to compare `bson.D` documents.
The differences in field order, types, missing fields and others should be reported.
If differences are expected such as `version` field, only the field key can be used for `bson.D` document comparison.
Use of (deprecated) `bson.D.Map()` or converting `bson.D` to other types using helpers should be avoided when possible.
The bar for adding new helpers is very high.
Please check all existing ones.
@@ -297,30 +303,42 @@ For example:
const doubleMaxPrec = float64(1<<53 - 1) // 9007199254740991.0: largest double values that could be represented as integer exactly
```
Avoid writing tests that cannot run in parallel with other tests.
The `integration` directory contains multiple test packages,
and tests from other packages may run at the same time.
Such test becomes flaky and hard to maintain.
#### Integration tests naming guidelines
<!-- https://github.com/FerretDB/FerretDB/issues/694 -->
1. Test names should include the name of the command being tested.
For instance, `TestDistinct` for testing the distinct command.
Use `Test<Command><Operator><Scenario>` format for test names, omitting `<Operator>` and `<Scenario>` if unnecessary.
1. Include the name of the command (and the operator if any).
For example, `TestDistinct` for testing the distinct command.
2. Compatibility tests should have `Compat` in the name, following the command.
For example, `TestDistinctCompat`.
3. If the test doesn't use driver method but runs a command directly via `RunCommand`,
the suffix `Command` should be added.
3. If the test runs a command directly via `RunCommand`, add the suffix `Command`.
For example, `TestDistinctCommand`.
4. If the test is both compat and runs a command, the suffix `CommandCompat` should be added.
4. If the test is both compat and runs a command, add the suffix `CommandCompat`.
For example, `TestInsertCommandCompat`.
5. If the file consists of compatibility tests, add the `_compat` suffix.
For example, `distinct_compat_test.go`.
6. Test names should be descriptive and provide information about the functionality or condition being tested.
If the test is checking for a specific error scenario, include the error scenario in the name.
6. Use descriptive test names to include the condition or the specific error being tested.
For example, `TestAggregateCompatSortDotNotation` for compatibility tests
on `aggregate` command `$sort` operator on a document field.
7. Keep test names concise, avoiding overly cryptic names.
Use abbreviations when appropriate.
8. Avoid including test data in the name to maintain clarity and prevent excessively long names.
9. Test case names should follow `TitleCase` capitalization style.
9. Use `TitleCase` capitalization style for test case names.
No spaces, dashes or underscores should be used neither for test names nor for test case names.
10. Keep the concatenation of test name segments (test, subtests) within 64 characters
10. Use [BSON notation](https://bsonspec.org/spec.html) vocabulary.
For example, use `Double` and not `float64`.
11. Keep the concatenation of test name segments (test, subtests) within 64 characters
to satisfy the maximum limit for database names.
12. Use a package for tests in the same scope.
Separating into packages can reduce binary sizes and may enhance performance.
For example, use `users` package for user management test files.
### Submitting code changes
@@ -389,4 +407,4 @@ Before submitting a pull request, please make sure that:
created from the [current state](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)
of our main branch.
2. Documentation is formatted, linted, and built with `task docs`.
3. Documentation is written according to our [writing guide](https://docs.ferretdb.io/v1.24/contributing/writing-guide/).
3. Documentation is written according to our [writing guide](https://docs.ferretdb.io/contributing/writing-guide/).
+38 -42
View File
@@ -1,28 +1,42 @@
# FerretDB
[![Go Reference](https://pkg.go.dev/badge/github.com/FerretDB/FerretDB/ferretdb.svg)](https://pkg.go.dev/github.com/FerretDB/FerretDB/ferretdb)
<!-- textlint-disable one-sentence-per-line -->
> [!TIP]
> Looking for FerretDB v1?
> It is [there](https://github.com/FerretDB/FerretDB/tree/main-v1).
<!-- textlint-enable one-sentence-per-line -->
[![Go](https://github.com/FerretDB/FerretDB/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/FerretDB/FerretDB/actions/workflows/go.yml)
[![codecov](https://codecov.io/gh/FerretDB/FerretDB/branch/main-v1/graph/badge.svg?token=JZ56XFT3DM)](https://codecov.io/gh/FerretDB/FerretDB)
[![codecov](https://codecov.io/gh/FerretDB/FerretDB/branch/main/graph/badge.svg?token=JZ56XFT3DM)](https://codecov.io/gh/FerretDB/FerretDB)
[![Security](https://github.com/FerretDB/FerretDB/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/FerretDB/FerretDB/actions/workflows/security.yml)
[![Packages](https://github.com/FerretDB/FerretDB/actions/workflows/packages.yml/badge.svg?branch=main)](https://github.com/FerretDB/FerretDB/actions/workflows/packages.yml)
[![Docs](https://github.com/FerretDB/FerretDB/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/FerretDB/FerretDB/actions/workflows/docs.yml)
FerretDB was founded to become the de-facto open-source substitute to MongoDB.
FerretDB is an open-source proxy, converting the MongoDB 5.0+ wire protocol queries to SQL -
using PostgreSQL or SQLite as a database engine.
<!-- markdownlint-disable MD033 -->
<img src="website/static/ferrets-are-working.svg" width="421" height="298" alt="Ferrets are working" title="Ferrets are working">
<!-- markdownlint-enable MD033 -->
_Please pardon the dust as we move FerretDB v2 to this repository._
---
FerretDB is an open-source alternative to MongoDB.
It is a proxy that converts MongoDB 5.0+ wire protocol queries to SQL
and uses PostgreSQL with [DocumentDB extension](https://github.com/microsoft/documentdb) as a database engine.
```mermaid
flowchart LR
A["Any application<br>Any MongoDB driver"]
F{{FerretDB}}
P[(PostgreSQL)]
S[("SQLite")]
P[(PostgreSQL<br>DocumentDB extension)]
A -- "MongoDB protocol<br>BSON" --> F
F -- "PostgreSQL protocol<br>SQL" --> P
F -. "SQLite library<br>SQL" .-> S
```
## Why do we need FerretDB?
@@ -31,7 +45,7 @@ MongoDB was originally an eye-opening technology for many of us developers,
empowering us to build applications faster than using relational databases.
In its early days, its ease-to-use and well-documented drivers made MongoDB one of the simplest database solutions available.
However, as time passed, MongoDB abandoned its open-source roots;
changing the license to [SSPL](https://www.mongodb.com/licensing/server-side-public-license) - making it unusable for many open source and early-stage commercial projects.
changing the license to [SSPL](https://www.ferretdb.com/sspl) - making it unusable for many open-source and early-stage commercial projects.
Most MongoDB users do not require any advanced features offered by MongoDB;
however, they need an easy-to-use open-source document database solution.
@@ -45,62 +59,47 @@ Features are constantly being added to further increase compatibility and perfor
We welcome all contributors.
See our [public roadmap](https://github.com/orgs/FerretDB/projects/2/views/1),
a list of [known differences with MongoDB](https://docs.ferretdb.io/v1.24/diff/),
a list of [known differences with MongoDB](https://docs.ferretdb.io/diff/),
and [contributing guidelines](CONTRIBUTING.md).
## Quickstart
Run this command to start FerretDB with PostgreSQL backend:
Run this command to start FerretDB with PostgreSQL:
```sh
docker run -d --rm --name ferretdb -p 27017:27017 ghcr.io/ferretdb/all-in-one
docker run -d --rm --name ferretdb -p 27017:27017 ghcr.io/ferretdb/ferretdb-eval:2
```
Alternatively, run this command to start FerretDB with SQLite backend:
```sh
docker run -d --rm --name ferretdb -p 27017:27017 -e FERRETDB_HANDLER=sqlite ghcr.io/ferretdb/all-in-one
```
This command will start a container with FerretDB, PostgreSQL/SQLite, and MongoDB Shell for quick testing and experiments.
This command will start a container with FerretDB, pre-packaged PostgreSQL with DocumentDB extension, and MongoDB Shell for quick testing and experiments.
However, it is unsuitable for production use cases because it keeps all data inside and loses it on shutdown.
See our [Docker quickstart guide](https://docs.ferretdb.io/v1.24/quickstart-guide/docker/) for instructions
See our [installation guides](https://docs.ferretdb.io/installation/) for instructions
that don't have those problems.
With that container running, you can:
- Connect to it with any MongoDB client application using MongoDB URI `mongodb://127.0.0.1:27017/`.
- Connect to it with any MongoDB client application using MongoDB URI `mongodb://username:password@127.0.0.1:27017/`.
- Connect to it using MongoDB Shell by just running `mongosh`.
If you don't have it installed locally, you can run `docker exec -it ferretdb mongosh`.
- For the PostgreSQL backend, connect to it by running `docker exec -it ferretdb psql -U username ferretdb`.
FerretDB uses PostgreSQL schemas for MongoDB databases.
So, if you created some collections in the `test` database using any MongoDB client,
you can switch to it by running `SET search_path = 'test';` query
and see a list of PostgreSQL tables by running `\d` `psql` command.
- For the SQLite backend, connect to it by running `docker exec -it ferretdb sqlite3 /state/<database>.sqlite`.
So, if you created some collections in the `test` database using any MongoDB client,
run `docker exec -it ferretdb sqlite3 /state/test.sqlite`
and see a list of SQLite tables by running `.tables` command.
- For PostgreSQL, connect to it by running `docker exec -it ferretdb psql -U username postgres`.
You can stop the container with `docker stop ferretdb`.
We also provide binaries and packages for various Linux distributions,
as well as [Go library package](https://pkg.go.dev/github.com/FerretDB/FerretDB/ferretdb) that embeds FerretDB into your application.
See [our documentation](https://docs.ferretdb.io/v1.24/quickstart-guide/) for more details.
We also provide binaries and packages for various Linux distributions.
See [our documentation](https://docs.ferretdb.io/installation/) for more details.
## Building and packaging
<!-- textlint-disable one-sentence-per-line -->
> [!NOTE]
> We strongly advise users not to build FerretDB themselves.
> We advise users not to build FerretDB themselves.
> Instead, use binaries, Docker images, or packages provided by us.
<!-- textlint-enable one-sentence-per-line -->
FerretDB could be built as any other Go program,
but a few generated files and build tags could affect it.
See [there](https://pkg.go.dev/github.com/FerretDB/FerretDB/build/version) for more details.
See [there](https://pkg.go.dev/github.com/FerretDB/FerretDB/v2/build/version) for more details.
## Managed FerretDB at cloud providers
@@ -109,15 +108,12 @@ See [there](https://pkg.go.dev/github.com/FerretDB/FerretDB/build/version) for m
- [Elestio](https://elest.io/open-source/ferretdb)
- [Cozystack](https://cozystack.io/docs/components/#managed-ferretdb).
## Documentation
- [Documentation for users](https://docs.ferretdb.io/).
- [Documentation for Go developers about embeddable FerretDB](https://pkg.go.dev/github.com/FerretDB/FerretDB/ferretdb).
## Community
- Website and blog: https://www.ferretdb.com/.
- Twitter: [@ferret_db](https://twitter.com/ferret_db).
- Website: https://www.ferretdb.com/.
- Blog: https://blog.ferretdb.io/.
- Documentation: https://docs.ferretdb.io/.
- X (Twitter): [@ferret_db](https://x.com/ferret_db).
- Mastodon: [@ferretdb@techhub.social](https://techhub.social/@ferretdb).
- [Slack chat](https://join.slack.com/t/ferretdb/shared_invite/zt-zqe9hj8g-ZcMG3~5Cs5u9uuOPnZB8~A) for quick questions.
- [GitHub Discussions](https://github.com/FerretDB/FerretDB/discussions) for longer topics.
+123 -247
View File
@@ -8,14 +8,15 @@ env:
vars:
SHARD_INDEX: 0
SHARD_TOTAL: 0
DISABLE_PUSHDOWN: false
TEST_RUN: ""
TEST_TIMEOUT: 35m
NO_XFAIL: false
BENCH_TIME: 5s
TESTJS_PORT: 27017
RACE_FLAG: -race={{and (ne OS "windows") (ne ARCH "arm") (ne ARCH "riscv64")}}
BUILD_TAGS: ferretdb_debug,ferretdb_hana
SERVICES: postgres postgres_secured mysql mongodb mongodb_secured jaeger
BUILD_TAGS: ferretdb_dev
MODE: diff-normal
SERVICES: postgres mongodb mongodb-secure jaeger
tasks:
# invoked when `task` is run without arguments
@@ -30,7 +31,6 @@ tasks:
- task: test-unit
- task: lint
- task: security
- task: fmt-yaml
- task: docs
init-tools:
@@ -105,6 +105,11 @@ tasks:
- docker compose ps --all
- docker compose logs --follow
env-logs-collect:
cmds:
- docker compose ps --all
- docker compose logs
env-up:
desc: "Start development environment"
deps: [env-up-detach, env-setup]
@@ -153,17 +158,18 @@ tasks:
desc: "Generate (and format) Go code"
cmds:
- go generate -x ./...
#- bin/deadcode{{exeExt}} -generated -test -filter=documentdb_api ./cmd/ferretdb
- bin/task{{exeExt}} -d integration integration-gen
- task: fmt-go
# invoked by FerretDB/github-actions/linters action
fmt:
deps: [fmt-go]
# invoked by FerretDB/github-actions/linters action
fmt-go:
desc: "Format Go code"
cmds:
- bin/goimports{{exeExt}} -local=github.com/FerretDB/FerretDB -w .
- bin/goimports{{exeExt}} -local=github.com/FerretDB/FerretDB/v2 -w .
- bin/gofumpt{{exeExt}} -w .
test:
@@ -173,26 +179,22 @@ tasks:
test-unit-short:
desc: "Run short unit tests (with caching)"
cmds:
- go test -short -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -coverpkg=./... -coverprofile=cover.txt ./...
- go test -short -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -coverprofile=cover.txt ./...
- bin/task{{exeExt}} -d tools tools-test-short
test-unit:
desc: "Run all unit tests"
cmds:
- go test -count=1 -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -coverpkg=./... -coverprofile=cover.txt ./...
- go test -count=1 -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -coverprofile=cover.txt ./...
- go test -count=1 -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -bench=. -benchtime=1x ./...
- bin/task{{exeExt}} -d tools tools-test
test-integration:
desc: "Run integration tests for several backends in parallel"
deps:
- test-integration-postgresql
- test-integration-sqlite
# no test-integration-hana
- test-integration-mongodb
desc: "Run integration tests in parallel"
deps: [test-integration-postgresql, test-integration-mongodb]
test-integration-postgresql:
desc: "Run integration tests for `postgresql` backend"
desc: "Run integration tests for PostgreSQL/DocumentDB"
deps: [gen-version]
dir: integration
cmds:
@@ -202,6 +204,7 @@ tasks:
--shard-total={{.SHARD_TOTAL}}
--run='{{.TEST_RUN}}'
--skip='{{.TEST_SKIP}}'
--raw-prefix=postgresql-{{.SHARD_INDEX}}-{{.SHARD_TOTAL}}
--
-count=1
-timeout={{.TEST_TIMEOUT}}
@@ -211,89 +214,10 @@ tasks:
-coverpkg=../...
-coverprofile=integration-postgresql.txt
./...
-target-backend=ferretdb-postgresql
-target-tls
-postgresql-url='postgres://username@127.0.0.1:5432/ferretdb?search_path='
-compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=client.pem&tlsCaFile=rootCA-cert.pem&replicaSet=rs0'
-disable-pushdown={{.DISABLE_PUSHDOWN}}
test-integration-sqlite:
desc: "Run integration tests for `sqlite` backend"
deps: [gen-version]
dir: integration
cmds:
- >
../bin/envtool{{exeExt}} tests run
--shard-index={{.SHARD_INDEX}}
--shard-total={{.SHARD_TOTAL}}
--run='{{.TEST_RUN}}'
--skip='{{.TEST_SKIP}}'
--
-count=1
-timeout={{.TEST_TIMEOUT}}
{{.RACE_FLAG}}
-tags={{.BUILD_TAGS}}
-shuffle=on
-coverpkg=../...
-coverprofile=integration-sqlite.txt
./...
-target-backend=ferretdb-sqlite
-sqlite-url=file:../tmp/sqlite-tests/
-target-tls
-compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=client.pem&tlsCaFile=rootCA-cert.pem&replicaSet=rs0'
-disable-pushdown={{.DISABLE_PUSHDOWN}}
test-integration-mysql:
desc: "Run integration tests for `mysql` handler"
deps: [gen-version]
dir: integration
cmds:
- >
../bin/envtool{{exeExt}} tests run
--shard-index={{.SHARD_INDEX}}
--shard-total={{.SHARD_TOTAL}}
--run='{{.TEST_RUN}}'
--skip='{{.TEST_SKIP}}'
--
-count=1
-timeout={{.TEST_TIMEOUT}}
{{.RACE_FLAG}}
-tags={{.BUILD_TAGS}}
-shuffle=on
-coverpkg=../...
-coverprofile=integration-mysql.txt
./...
-target-backend=ferretdb-mysql
-target-tls
-mysql-url='mysql://username:password@127.0.0.1:3306/ferretdb'
-compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=client.pem&tlsCaFile=rootCA-cert.pem&replicaSet=rs0'
-disable-pushdown={{.DISABLE_PUSHDOWN}}
test-integration-hana:
desc: "Run integration tests for `hana` handler"
deps: [gen-version]
dir: integration
cmds:
- >
../bin/envtool{{exeExt}} tests run
--shard-index={{.SHARD_INDEX}}
--shard-total={{.SHARD_TOTAL}}
--run='{{.TEST_RUN}}'
--skip='{{.TEST_SKIP}}'
--
-count=1
-timeout={{.TEST_TIMEOUT}}
{{.RACE_FLAG}}
-tags={{.BUILD_TAGS}}
-shuffle=on
-coverpkg=../...
-coverprofile=integration-hana.txt
./...
-target-backend=ferretdb-hana
-target-tls
-hana-url=$FERRETDB_HANA_URL
-compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=client.pem&tlsCaFile=rootCA-cert.pem&replicaSet=rs0'
-disable-pushdown={{.DISABLE_PUSHDOWN}}
-target-backend=ferretdb
-postgresql-url='postgres://username:password@127.0.0.1:5432/postgres'
-compat-url='mongodb://username:password@127.0.0.1:47017/?replicaSet=rs0'
-no-xfail={{.NO_XFAIL}}
test-integration-mongodb:
desc: "Run integration tests for MongoDB"
@@ -306,6 +230,7 @@ tasks:
--shard-total={{.SHARD_TOTAL}}
--run='{{.TEST_RUN}}'
--skip='{{.TEST_SKIP}}'
--raw-prefix=mongodb-{{.SHARD_INDEX}}-{{.SHARD_TOTAL}}
--
-count=1
-timeout={{.TEST_TIMEOUT}}
@@ -315,8 +240,9 @@ tasks:
-coverpkg=../...
-coverprofile=integration-mongodb.txt
./...
-target-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=client.pem&tlsCaFile=rootCA-cert.pem&replicaSet=rs0'
-target-url='mongodb://username:password@127.0.0.1:47017/?replicaSet=rs0'
-target-backend=mongodb
-no-xfail={{.NO_XFAIL}}
bench-unit:
desc: "Run unit benchmarks"
@@ -326,124 +252,36 @@ tasks:
- bin/benchstat{{exeExt}} old.txt new.txt
run:
desc: "Run FerretDB with `postgresql` backend"
desc: "Run FerretDB without auth"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--mode={{.MODE}}
--no-auth
--listen-addr=:27017
--proxy-addr=127.0.0.1:47017
--mode=diff-normal
--repl-set-name=rs0
--handler=pg
--postgresql-url='postgres://username@127.0.0.1:5432/ferretdb?search_path='
--test-records-dir=tmp/records
--listen-data-api-addr=:8899
--proxy-addr=127.0.0.1:37017
--postgresql-url='postgres://username:password@127.0.0.1:5432/postgres'
--dev-records-dir=tmp/records
--otel-traces-url=http://127.0.0.1:4318/v1/traces
run-sqlite:
desc: "Run FerretDB with `sqlite` backend"
run-secure:
desc: "Run FerretDB with auth"
deps: [build-host]
cmds:
- bin/envtool{{exeExt}} shell mkdir tmp/sqlite
- >
bin/ferretdb{{exeExt}}
--mode={{.MODE}}
--listen-addr=:27017
--listen-data-api-addr=:8899
--proxy-addr=127.0.0.1:47017
--mode=diff-normal
--handler=sqlite
--sqlite-url=file:tmp/sqlite/
--test-records-dir=tmp/records
run-mysql:
desc: "Run FerretDB with `mysql` backend"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--listen-addr=:27017
--proxy-addr=127.0.0.1:47017
--mode=diff-normal
--handler=mysql
--mysql-url='mysql://username@127.0.0.1:3306/ferretdb'
--test-records-dir=tmp/records
# set FERRETDB_HANA_URL environment variable to use it
run-hana:
desc: "Run FerretDB with `hana` backend"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--listen-addr=:27017
--proxy-addr=127.0.0.1:47017
--mode=diff-normal
--handler=hana
--test-records-dir=tmp/records
run-secured:
desc: "Run FerretDB with `postgresql` backend (TLS, auth required)"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--listen-addr=''
--listen-tls=:27018
--listen-tls-cert-file=./build/certs/server-cert.pem
--listen-tls-key-file=./build/certs/server-key.pem
--listen-tls-ca-file=./build/certs/rootCA-cert.pem
--proxy-addr=127.0.0.1:47017
--mode=diff-normal
--handler=pg
--postgresql-url='postgres://127.0.0.1:5433/ferretdb?search_path='
--test-records-dir=tmp/records
run-proxy:
desc: "Run FerretDB in the diff-proxy mode"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--listen-addr=:27017
--proxy-addr=127.0.0.1:47017
--mode=diff-proxy
--handler=pg
--postgresql-url='postgres://username@127.0.0.1:5432/ferretdb?search_path='
--test-records-dir=tmp/records
run-sqlite-proxy:
desc: "Run FerretDB with `sqlite` handler in the diff-proxy mode"
deps: [build-host]
cmds:
- bin/envtool{{exeExt}} shell mkdir tmp/sqlite
- >
bin/ferretdb{{exeExt}}
--listen-addr=:27017
--proxy-addr=127.0.0.1:47017
--mode=diff-proxy
--handler=sqlite
--sqlite-url=file:tmp/sqlite/
--test-records-dir=tmp/records
run-proxy-secured:
desc: "Run FerretDB in the diff-proxy mode (TLS, auth required)"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--listen-addr=''
--listen-tls=:27018
--listen-tls-cert-file=./build/certs/server-cert.pem
--listen-tls-key-file=./build/certs/server-key.pem
--listen-tls-ca-file=./build/certs/rootCA-cert.pem
--proxy-addr=127.0.0.1:47018
--proxy-tls-cert-file=./build/certs/client-cert.pem
--proxy-tls-key-file=./build/certs/client-key.pem
--proxy-tls-ca-file=./build/certs/rootCA-cert.pem
--mode=diff-proxy
--handler=pg
--postgresql-url='postgres://username@127.0.0.1:5433/ferretdb?search_path='
--test-records-dir=tmp/records
--repl-set-name=rs0
--postgresql-url='postgres://username:password@127.0.0.1:5432/postgres'
--dev-records-dir=tmp/records
--otel-traces-url=http://127.0.0.1:4318/v1/traces
# invoked by FerretDB/github-actions/linters action
lint:
desc: "Run linters"
cmds:
@@ -451,8 +289,7 @@ tasks:
- bin/envtool{{exeExt}} shell mkdir tmp/githubcache
- bin/golangci-lint{{exeExt}} run --config=.golangci.yml
- bin/golangci-lint{{exeExt}} run --config=.golangci-new.yml
- bin/go-consistent{{exeExt}} -pedantic ./cmd/... ./internal/... ./build/... ./ferretdb/...
- bin/go-consistent{{exeExt}} -pedantic ./cmd/... ./internal/... ./build/...
- go vet -vettool=bin/checkswitch{{exeExt}} ./...
- go vet -vettool=bin/checkcomments{{exeExt}} ./...
@@ -483,17 +320,7 @@ tasks:
psql:
desc: "Run psql"
cmds:
- docker compose exec -e PGPASSWORD=password postgres psql -U username -d ferretdb
sqlite3:
desc: "Run sqlite3"
cmds:
- sqlite3 tmp/sqlite/*.sqlite
mysql:
desc: "Run mysql"
cmds:
- docker compose exec -e MYSQL_PASSWORD=password mysql mysql -u username -ppassword ferretdb
- docker compose exec postgres psql -p 5432 -U username -d postgres
mongosh:
desc: "Run MongoDB shell (`mongosh`)"
@@ -503,17 +330,32 @@ tasks:
--verbose --eval 'disableTelemetry()' --shell
'mongodb://host.docker.internal:27017/?heartbeatFrequencyMS=300000'
mongosh-secured:
desc: "Run MongoDB shell (`mongosh`) with TLS and auth"
mongosh-secure:
desc: "Run MongoDB shell (`mongosh`) with auth"
cmds:
- >
docker compose exec mongodb mongosh
--verbose --eval 'disableTelemetry()' --shell
'mongodb://username:password@host.docker.internal:27018/?authMechanism=PLAIN&tls=true&tlsCertificateKeyFile=/etc/certs/client.pem&tlsCaFile=/etc/certs/rootCA-cert.pem'
'mongodb://username:password@host.docker.internal:27017/?heartbeatFrequencyMS=300000'
mongo:
desc: "Run legacy MongoDB shell (`mongo`)"
cmds:
- >
docker compose run --rm legacy-mongo-shell
'mongodb://username:password@host.docker.internal:27017/'
testsql:
desc: "Run pgTAP with test.sql script"
cmds:
- docker compose exec postgres pg_prove --ext .sql --verbose -p 5432 -U username -d postgres /testscripts/
testjs:
desc: "Run legacy MongoDB shell (`mongo`) with test.js script"
cmds:
# Authentication with username:password will not work
# because this user is created by setup with a different salt length,
# and there are no other credentials to use.
- >
docker compose run --rm legacy-mongo-shell
'mongodb://host.docker.internal:{{.TESTJS_PORT}}/'
@@ -529,7 +371,6 @@ tasks:
--bootstrap=true
--use=false
--config=./build/buildkitd.toml
--driver=docker-container
--driver-opt network=host
--driver-opt env.JAEGER_TRACE=127.0.0.1:6831
--driver-opt env.BUILDKIT_STEP_LOG_MAX_SIZE=-1
@@ -578,7 +419,7 @@ tasks:
vars: [DOCKER_ARCH, DIR_ARCH, BIN_FILE, BIN_DIR]
build-development:
desc: "Build development debug binaries"
desc: "Build development binaries"
cmds:
- echo 'bin-development' > build/version/package.txt
- bin/envtool{{exeExt}} shell rmdir tmp/build
@@ -587,9 +428,9 @@ tasks:
vars:
FILE: ferretdb/development
TARGET: development-binary
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=local,dest=tmp/build
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: build-move-check
vars:
DOCKER_ARCH: "{{.ITEM}}" # arm/v7
@@ -598,7 +439,7 @@ tasks:
BIN_DIR: bin-dev
build-production:
desc: "Build production non-debug binaries"
desc: "Build production binaries"
cmds:
- echo 'bin' > build/version/package.txt
- bin/envtool{{exeExt}} shell rmdir tmp/build
@@ -607,9 +448,9 @@ tasks:
vars:
FILE: ferretdb/production
TARGET: production-binary
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=local,dest=tmp/build
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: build-move-check
vars:
DOCKER_ARCH: "{{.ITEM}}" # arm/v7
@@ -631,14 +472,14 @@ tasks:
vars: [FILE]
# TODO https://github.com/FerretDB/FerretDB/issues/2212
docker-all-in-one-push:
docker-evaluation-push:
cmds:
- echo 'docker-all-in-one' > build/version/package.txt
- echo 'docker-evaluation' > build/version/package.txt
- task: docker-build
vars:
FILE: ferretdb/all-in-one
TARGET: all-in-one
PLATFORM: linux/amd64,linux/arm64 # no mongosh for arm/v6 and arm/v7
FILE: ferretdb/evaluation
TARGET: evaluation
PLATFORM: linux/amd64 # no DocumentDB for linux/arm64 yet
OUTPUT: type=image,push=true
requires:
vars: [DOCKER_IMAGES]
@@ -651,7 +492,7 @@ tasks:
vars:
FILE: ferretdb/development
TARGET: development
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=image,push=true
requires:
vars: [DOCKER_IMAGES]
@@ -664,7 +505,42 @@ tasks:
vars:
FILE: ferretdb/production
TARGET: production
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=image,push=true
requires:
vars: [DOCKER_IMAGES]
docker-documentdb:
desc: "Build `documentdb-local` Docker image"
cmds:
- task: docker-build
vars:
FILE: postgres-documentdb/{{.FILE}}
TARGET: "{{.FILE}}"
PLATFORM: linux/amd64
DOCKER_IMAGES: documentdb-local
OUTPUT: type=docker
requires:
vars: [FILE]
docker-documentdb-development-push:
cmds:
- task: docker-build
vars:
FILE: postgres-documentdb/development
TARGET: development
PLATFORM: linux/amd64
OUTPUT: type=image,push=true
requires:
vars: [DOCKER_IMAGES]
docker-documentdb-production-push:
cmds:
- task: docker-build
vars:
FILE: postgres-documentdb/production
TARGET: production
PLATFORM: linux/amd64
OUTPUT: type=image,push=true
requires:
vars: [DOCKER_IMAGES]
@@ -713,9 +589,9 @@ tasks:
vars:
FILE: ferretdb/development
TARGET: development-binary
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=local,dest=tmp/build
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-build
vars:
PACKAGER: deb
@@ -723,7 +599,7 @@ tasks:
PACKAGE_ARCH: '{{replace "/" "" .ITEM | replace "v" ""}}' # arm/v7 -> arm7
DIR_ARCH: '{{replace "/" "_" .ITEM}}' # arm/v7 -> arm_v7
FILE_SUFFIX: 'dev-linux-{{replace "/" "" .ITEM}}' # arm/v7 -> armv7
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-deb-check
vars:
DOCKER_ARCH: "{{.ITEM}}" # arm/v7
@@ -741,9 +617,9 @@ tasks:
vars:
FILE: ferretdb/production
TARGET: production-binary
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=local,dest=tmp/build
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-build
vars:
PACKAGER: deb
@@ -751,7 +627,7 @@ tasks:
PACKAGE_ARCH: '{{replace "/" "" .ITEM | replace "v" ""}}' # arm/v7 -> arm7
DIR_ARCH: '{{replace "/" "_" .ITEM}}' # arm/v7 -> arm_v7
FILE_SUFFIX: 'linux-{{replace "/" "" .ITEM}}' # arm/v7 -> armv7
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-deb-check
vars:
DOCKER_ARCH: "{{.ITEM}}" # arm/v7
@@ -781,9 +657,9 @@ tasks:
vars:
FILE: ferretdb/development
TARGET: development-binary
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=local,dest=tmp/build
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-build
vars:
PACKAGER: rpm
@@ -791,7 +667,7 @@ tasks:
PACKAGE_ARCH: '{{replace "/" "" .ITEM | replace "v" ""}}' # arm/v7 -> arm7
DIR_ARCH: '{{replace "/" "_" .ITEM}}' # arm/v7 -> arm_v7
FILE_SUFFIX: 'dev-linux-{{replace "/" "" .ITEM}}' # arm/v7 -> armv7
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-rpm-check
vars:
DOCKER_ARCH: "{{.ITEM}}" # arm/v7
@@ -809,9 +685,9 @@ tasks:
vars:
FILE: ferretdb/production
TARGET: production-binary
PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 # TODO https://github.com/FerretDB/FerretDB/issues/3354
PLATFORM: linux/amd64,linux/arm64
OUTPUT: type=local,dest=tmp/build
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-build
vars:
PACKAGER: rpm
@@ -819,7 +695,7 @@ tasks:
PACKAGE_ARCH: '{{replace "/" "" .ITEM | replace "v" ""}}' # arm/v7 -> arm7
DIR_ARCH: '{{replace "/" "_" .ITEM}}' # arm/v7 -> arm_v7
FILE_SUFFIX: 'linux-{{replace "/" "" .ITEM}}' # arm/v7 -> armv7
- for: [amd64, arm64, arm/v7]
- for: [amd64, arm64]
task: packages-rpm-check
vars:
DOCKER_ARCH: "{{.ITEM}}" # arm/v7
@@ -857,7 +733,7 @@ tasks:
"**/*.md" "**/*.mdx"
- >
docker compose run --rm markdownlint --config "build/.markdownlint.yml"
"**/*.md" "**/*.mdx"
"**/*.md" "**/*.mdx" "#build/postgres-documentdb/documentdb/"
- bin/checkdocs
docs-fmt:
@@ -925,7 +801,7 @@ tasks:
- docker compose run --rm --service-ports wrangler pages deployment create --project-name=blog website/build
changelog:
desc: "Generate changelogs locally for a given milestone."
desc: "Generate changelogs locally for a given milestone"
cmds:
- bin/generatechangelog -milestone={{.MILESTONE_TITLE}} -previous={{.PREVIOUS_MILESTONE_TITLE}}
requires:
-1
View File
@@ -1 +0,0 @@
FROM citusdata/citus:12.1
-1
View File
@@ -1 +0,0 @@
FROM mongo:7.0.8
+15
View File
@@ -0,0 +1,15 @@
# syntax=docker/dockerfile:1
FROM mongo:7.0.8
# If you encounter an "unknown instruction" error there,
# please update Docker to the latest version.
# see https://github.com/docker-library/mongo/issues/475
RUN <<EOF
set -ex
echo 'topsecret' > /etc/mongod_keyfile.txt
chmod 0400 /etc/mongod_keyfile.txt
chown 999:999 /etc/mongod_keyfile.txt
EOF
-1
View File
@@ -1 +0,0 @@
FROM mysql:8.4.0
@@ -0,0 +1,2 @@
FROM ghcr.io/ferretdb/postgres-documentdb-dev:16.6-0.100-0
# FROM ghcr.io/ferretdb/postgres-documentdb:16.6-0.100-0
-1
View File
@@ -1 +0,0 @@
FROM postgres:16.4
+2 -2
View File
@@ -9,9 +9,9 @@ ExecStart=/usr/bin/ferretdb
Restart=on-failure
# Configure the FerretDB service with `systemctl edit ferretdb`.
# For more configuration options check https://docs.ferretdb.io/v1.24/configuration/flags/
# For more configuration options check https://docs.ferretdb.io/configuration/flags/
Environment="FERRETDB_POSTGRESQL_URL=postgres://127.0.0.1:5432/ferretdb"
Environment="FERRETDB_POSTGRESQL_URL=postgres://127.0.0.1:5432/postgres"
[Install]
WantedBy=multi-user.target
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
/usr/local/bin/ferretdb &
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
set -ex
exec /usr/bin/runsvdir /etc/service
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
set -ex
exec /ferretdb
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
set -ex
exec /usr/local/bin/docker-entrypoint.sh postgres
+5 -12
View File
@@ -14,7 +14,7 @@ ARG LABEL_COMMIT
FROM --platform=$BUILDPLATFORM golang:1.23.5 AS development-prepare
# use a single directory for all Go caches to simpliy RUN --mount commands below
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
ENV GOCACHE=/cache/gocache
ENV GOMODCACHE=/cache/gomodcache
@@ -39,7 +39,6 @@ EOF
FROM golang:1.23.5 AS development-build
ARG TARGETARCH
ARG TARGETVARIANT
ARG LABEL_VERSION
ARG LABEL_COMMIT
@@ -70,14 +69,10 @@ git status
# because v2+ is problematic for some virtualization platforms and older hardware.
export GOAMD64=v1
# Set GOARM explicitly due to https://github.com/docker-library/golang/issues/494.
export GOARM=${TARGETVARIANT#v}
export CGO_ENABLED=1
# Disable race detector on arm64 due to https://github.com/golang/go/issues/29948
# (and that happens on GitHub-hosted Actions runners).
# Also disable it on arm/v6 and arm/v7 because it is not supported there.
RACE=false
if test "$TARGETARCH" = "amd64"
then
@@ -91,7 +86,7 @@ go env
# check if stdlib was cached
go install -v -race=$RACE std
go build -v -o=bin/ferretdb -race=$RACE -tags=ferretdb_debug -coverpkg=./... ./cmd/ferretdb
go build -v -o=bin/ferretdb -race=$RACE -tags=ferretdb_dev -coverpkg=./... ./cmd/ferretdb
go version -m bin/ferretdb
bin/ferretdb --version
@@ -116,7 +111,7 @@ RUN mkdir /tmp/cover
COPY --from=development-build /src/bin/ferretdb /ferretdb
ENTRYPOINT ["/ferretdb"]
ENTRYPOINT [ "/ferretdb" ]
HEALTHCHECK --interval=1m --timeout=5s --retries=1 --start-period=30s --start-interval=5s \
CMD ["/ferretdb", "ping"]
@@ -130,17 +125,15 @@ ENV FERRETDB_LISTEN_ADDR=:27017
# ENV FERRETDB_LISTEN_TLS=:27018
ENV FERRETDB_DEBUG_ADDR=:8088
ENV FERRETDB_STATE_DIR=/state
ENV FERRETDB_SQLITE_URL=file:/state/
ARG LABEL_VERSION
ARG LABEL_COMMIT
# TODO https://github.com/FerretDB/FerretDB/issues/2212
LABEL org.opencontainers.image.description="A truly Open Source MongoDB alternative"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.description="A truly Open Source MongoDB alternative (development image)"
LABEL org.opencontainers.image.revision="${LABEL_COMMIT}"
LABEL org.opencontainers.image.source="https://github.com/FerretDB/FerretDB"
LABEL org.opencontainers.image.title="FerretDB"
LABEL org.opencontainers.image.title="FerretDB (development image)"
LABEL org.opencontainers.image.url="https://www.ferretdb.com/"
LABEL org.opencontainers.image.vendor="FerretDB Inc."
LABEL org.opencontainers.image.version="${LABEL_VERSION}"
@@ -1,8 +1,6 @@
# syntax=docker/dockerfile:1
# Dockerfile for all-in-one releases (`all-in-one` image).
# It always uses native compilation with race detector
# because packages are only available for amd64 and arm64 anyway.
# for evaluation (formerly "all-in-one") releases (`ferretdb-eval` image)
# While we already know commit and version from commit.txt and version.txt inside image,
# it is not possible to use them in LABELs for the final image.
@@ -14,9 +12,9 @@ ARG LABEL_COMMIT
# prepare stage
FROM --platform=$BUILDPLATFORM golang:1.23.5 AS all-in-one-prepare
FROM --platform=$BUILDPLATFORM golang:1.23.5 AS evaluation-prepare
# use a single directory for all Go caches to simpliy RUN --mount commands below
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
ENV GOCACHE=/cache/gocache
ENV GOMODCACHE=/cache/gomodcache
@@ -38,7 +36,7 @@ EOF
# build stage
FROM golang:1.23.5 AS all-in-one-build
FROM golang:1.23.5 AS evaluation-build
ARG TARGETARCH
@@ -60,7 +58,7 @@ WORKDIR /src
COPY . .
# to add a dependency
COPY --from=all-in-one-prepare /src/go.mod /src/go.sum /src/
COPY --from=evaluation-prepare /src/go.mod /src/go.sum /src/
RUN --mount=type=cache,target=/cache <<EOF
set -ex
@@ -71,13 +69,10 @@ git status
# because v2+ is problematic for some virtualization platforms and older hardware.
export GOAMD64=v1
# GOARM is not set because it is ignored for arm64
export CGO_ENABLED=1
# Disable race detector on arm64 due to https://github.com/golang/go/issues/29948
# (and that happens on GitHub-hosted Actions runners).
# Also disable it on arm/v6 and arm/v7 because it is not supported there.
RACE=false
if test "$TARGETARCH" = "amd64"
then
@@ -91,7 +86,7 @@ go env
# check if stdlib was cached
go install -v -race=$RACE std
go build -v -o=bin/ferretdb -race=$RACE -tags=ferretdb_debug -coverpkg=./... ./cmd/ferretdb
go build -v -o=bin/ferretdb -race=$RACE -tags=ferretdb_dev -coverpkg=./... ./cmd/ferretdb
go version -m bin/ferretdb
bin/ferretdb --version
@@ -100,26 +95,11 @@ EOF
# final stage
FROM postgres:16.4 AS all-in-one
COPY --from=all-in-one-build /src/bin/ferretdb /ferretdb
ENV GOCOVERDIR=/tmp/cover
ENV GORACE=halt_on_error=1,history_size=2
RUN mkdir /tmp/cover
# all-in-one hacks start there
COPY --from=all-in-one-build /src/build/ferretdb/all-in-one/ferretdb.sh /etc/service/ferretdb/run
COPY --from=all-in-one-build /src/build/ferretdb/all-in-one/postgresql.sh /etc/service/postgresql/run
COPY --from=all-in-one-build /src/build/ferretdb/all-in-one/entrypoint.sh /entrypoint.sh
FROM ghcr.io/ferretdb/postgres-documentdb:16.6-0.100-0 AS evaluation
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt <<EOF
set -ex
apt update
apt install -y curl runit sqlite3
mkdir /tmp/cover /tmp/state
chown postgres:postgres /tmp/cover /tmp/state
curl -L https://pgp.mongodb.com/server-7.0.asc | apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/7.0 main" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list
@@ -127,39 +107,37 @@ apt update
apt install -y mongodb-mongosh
EOF
ENV FERRETDB_POSTGRESQL_URL=postgres://username:password@127.0.0.1:5432/ferretdb
ENV POSTGRES_USER=username
ENV POSTGRES_PASSWORD=password
ENV POSTGRES_DB=ferretdb
STOPSIGNAL SIGHUP
ENTRYPOINT ["/entrypoint.sh"]
# all-in-one hacks stop there
COPY --from=evaluation-build /src/bin/ferretdb /usr/local/bin/ferretdb
COPY build/ferretdb/99-start-ferretdb.sh /docker-entrypoint-initdb.d/
HEALTHCHECK --interval=1m --timeout=5s --retries=1 --start-period=30s --start-interval=5s \
CMD ["/ferretdb", "ping"]
WORKDIR /
VOLUME /state
EXPOSE 27017 27018 8088
ENV GOCOVERDIR=/tmp/cover
ENV GORACE=halt_on_error=1,history_size=2
ENV POSTGRES_USER=username
ENV POSTGRES_PASSWORD=password
ENV POSTGRES_DB=postgres
ENV FERRETDB_POSTGRESQL_URL=postgres://username:password@127.0.0.1:5432/postgres
ENV FERRETDB_STATE_DIR=/tmp/state
# don't forget to update documentation if you change defaults
ENV FERRETDB_LISTEN_ADDR=:27017
# ENV FERRETDB_LISTEN_TLS=:27018
ENV FERRETDB_DEBUG_ADDR=:8088
ENV FERRETDB_STATE_DIR=/state
ENV FERRETDB_SQLITE_URL=file:/state/
ARG LABEL_VERSION
ARG LABEL_COMMIT
# TODO https://github.com/FerretDB/FerretDB/issues/2212
LABEL org.opencontainers.image.description="A truly Open Source MongoDB alternative"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.description="A truly Open Source MongoDB alternative (evaluation image)"
LABEL org.opencontainers.image.revision="${LABEL_COMMIT}"
LABEL org.opencontainers.image.source="https://github.com/FerretDB/FerretDB"
LABEL org.opencontainers.image.title="FerretDB"
LABEL org.opencontainers.image.title="FerretDB (evaluation image)"
LABEL org.opencontainers.image.url="https://www.ferretdb.com/"
LABEL org.opencontainers.image.vendor="FerretDB Inc."
LABEL org.opencontainers.image.version="${LABEL_VERSION}"
+2 -8
View File
@@ -14,7 +14,7 @@ ARG LABEL_COMMIT
FROM --platform=$BUILDPLATFORM golang:1.23.5 AS production-prepare
# use a single directory for all Go caches to simpliy RUN --mount commands below
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
ENV GOCACHE=/cache/gocache
ENV GOMODCACHE=/cache/gomodcache
@@ -39,7 +39,6 @@ EOF
FROM golang:1.23.5 AS production-build
ARG TARGETARCH
ARG TARGETVARIANT
ARG LABEL_VERSION
ARG LABEL_COMMIT
@@ -70,9 +69,6 @@ git status
# because v2+ is problematic for some virtualization platforms and older hardware.
export GOAMD64=v1
# Set GOARM explicitly due to https://github.com/docker-library/golang/issues/494.
export GOARM=${TARGETVARIANT#v}
export CGO_ENABLED=0
go env
@@ -108,7 +104,7 @@ USER ferretdb:ferretdb
COPY --from=production-build /src/bin/ferretdb /ferretdb
COPY --from=production-build --chown=ferretdb:ferretdb /state /state
ENTRYPOINT ["/ferretdb"]
ENTRYPOINT [ "/ferretdb" ]
HEALTHCHECK --interval=1m --timeout=5s --retries=1 --start-period=30s --start-interval=5s \
CMD ["/ferretdb", "ping"]
@@ -122,14 +118,12 @@ ENV FERRETDB_LISTEN_ADDR=:27017
# ENV FERRETDB_LISTEN_TLS=:27018
ENV FERRETDB_DEBUG_ADDR=:8088
ENV FERRETDB_STATE_DIR=/state
ENV FERRETDB_SQLITE_URL=file:/state/
ARG LABEL_VERSION
ARG LABEL_COMMIT
# TODO https://github.com/FerretDB/FerretDB/issues/2212
LABEL org.opencontainers.image.description="A truly Open Source MongoDB alternative"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.revision="${LABEL_COMMIT}"
LABEL org.opencontainers.image.source="https://github.com/FerretDB/FerretDB"
LABEL org.opencontainers.image.title="FerretDB"
-26
View File
@@ -1,26 +0,0 @@
// Copy to test.js and run with `task testjs`.
(function () {
"use strict";
const coll = db.test;
coll.drop();
const init = [
{ _id: "double", v: 42.13 },
{ _id: "double-whole", v: 42.0 },
{ _id: "double-zero", v: 0.0 },
];
coll.insertMany(init);
const query = { v: { $gt: 42.0 } };
const expected = [{ _id: "double", v: 42.13 }];
const actual = coll.find(query).toArray();
assert.eq(expected, actual);
print("test.js passed!");
})();
+21
View File
@@ -3,5 +3,26 @@
(function () {
"use strict";
// Update the following example with your test.
const coll = db.test;
coll.drop();
const init = [
{ _id: "double", v: 42.13 },
{ _id: "double-whole", v: 42.0 },
{ _id: "double-zero", v: 0.0 },
];
coll.insertMany(init);
const query = { v: { $gt: 42.0 } };
const expected = [{ _id: "double", v: 42.13 }];
const actual = coll.find(query).toArray();
assert.eq(expected, actual);
print("test.js passed!");
})();
+13
View File
@@ -0,0 +1,13 @@
net:
port: 47017 # matches host's port for replica set to work
bindIpAll: true
tls:
mode: disabled
security:
authorization: enabled
keyFile: /etc/mongod_keyfile.txt
replication:
oplogSizeMB: 512
replSetName: rs0
+1 -5
View File
@@ -1,12 +1,8 @@
net:
port: 47017 # matches host's port for replica set to work
port: 37017 # matches host's port for replica set to work
bindIpAll: true
tls:
mode: disabled
security:
authorization: disabled
replication:
oplogSizeMB: 512
replSetName: rs0
-16
View File
@@ -1,16 +0,0 @@
net:
port: 47018 # matches host's port for replica set to work
bindIpAll: true
tls:
mode: requireTLS
certificateKeyFile: /etc/certs/server.pem
CAFile: /etc/certs/rootCA-cert.pem
clusterFile: /etc/certs/client.pem
security:
authorization: enabled # that also enables authentication
clusterAuthMode: x509
replication:
oplogSizeMB: 512
replSetName: rs0
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
set -e
echo "shared_preload_libraries = 'pg_cron,pg_documentdb_core,pg_documentdb'" >> $PGDATA/postgresql.conf
echo "cron.database_name = 'postgres'" >> $PGDATA/postgresql.conf
source /usr/local/bin/docker-entrypoint.sh
docker_temp_server_stop
docker_temp_server_start "$@"
+1
View File
@@ -0,0 +1 @@
CREATE EXTENSION documentdb CASCADE;
@@ -0,0 +1 @@
CREATE EXTENSION pgtap;
@@ -0,0 +1,71 @@
# syntax=docker/dockerfile:1
FROM postgres:16.6 AS development
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US
ENV LC_ALL=en_US.UTF-8
ENV LC_CTYPE=en_US.UTF-8
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt <<EOF
set -ex
apt update
apt upgrade -y
apt install -y \
build-essential \
cmake \
curl \
git \
libicu-dev \
libkrb5-dev \
pkg-config \
postgresql-16-cron \
postgresql-16-pgvector \
postgresql-16-postgis-3 \
postgresql-16-rum \
postgresql-server-dev-16
EOF
# extra packages for development
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt <<EOF
set -ex
apt install -y \
bpftrace \
gdb \
postgresql-16-pgtap
EOF
RUN --mount=target=/src,rw <<EOF
set -ex
cd /src
mkdir -p /tmp/install_setup
cp build/postgres-documentdb/documentdb/scripts/* /tmp/install_setup/
cp build/postgres-documentdb/10-preload.sh build/postgres-documentdb/20-install.sql /docker-entrypoint-initdb.d/
cp build/postgres-documentdb/90-install-development.sql /docker-entrypoint-initdb.d/
export CLEANUP_SETUP=1
export INSTALL_DEPENDENCIES_ROOT=/tmp/install_setup
env MAKE_PROGRAM=cmake /tmp/install_setup/install_setup_libbson.sh
/tmp/install_setup/install_setup_pcre2.sh
/tmp/install_setup/install_setup_intel_decimal_math_lib.sh
cd build/postgres-documentdb/documentdb
make -k -j $(nproc) DEBUG=yes
make install
rm -fr /tmp/install_setup /var/lib/apt/lists/*
EOF
WORKDIR /
LABEL org.opencontainers.image.title="PostgreSQL+DocumentDB (development image)"
LABEL org.opencontainers.image.description="PostgreSQL with DocumentDB extension (development image)"
LABEL org.opencontainers.image.source="https://github.com/FerretDB/FerretDB"
LABEL org.opencontainers.image.url="https://www.ferretdb.com/"
LABEL org.opencontainers.image.vendor="FerretDB Inc."
@@ -0,0 +1,59 @@
# syntax=docker/dockerfile:1
FROM postgres:16.6 AS production
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US
ENV LC_ALL=en_US.UTF-8
ENV LC_CTYPE=en_US.UTF-8
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt <<EOF
set -ex
apt update
apt upgrade -y
apt install -y \
build-essential \
cmake \
curl \
git \
libicu-dev \
libkrb5-dev \
pkg-config \
postgresql-16-cron \
postgresql-16-pgvector \
postgresql-16-postgis-3 \
postgresql-16-rum \
postgresql-server-dev-16
EOF
RUN --mount=target=/src,rw <<EOF
set -ex
cd /src
mkdir -p /tmp/install_setup
cp build/postgres-documentdb/documentdb/scripts/* /tmp/install_setup/
cp build/postgres-documentdb/10-preload.sh build/postgres-documentdb/20-install.sql /docker-entrypoint-initdb.d/
export CLEANUP_SETUP=1
export INSTALL_DEPENDENCIES_ROOT=/tmp/install_setup
env MAKE_PROGRAM=cmake /tmp/install_setup/install_setup_libbson.sh
/tmp/install_setup/install_setup_pcre2.sh
/tmp/install_setup/install_setup_intel_decimal_math_lib.sh
cd build/postgres-documentdb/documentdb
make -k -j $(nproc)
make install
rm -fr /tmp/install_setup /var/lib/apt/lists/*
EOF
WORKDIR /
LABEL org.opencontainers.image.title="PostgreSQL+DocumentDB"
LABEL org.opencontainers.image.description="PostgreSQL with DocumentDB extension"
LABEL org.opencontainers.image.source="https://github.com/FerretDB/FerretDB"
LABEL org.opencontainers.image.url="https://www.ferretdb.com/"
LABEL org.opencontainers.image.vendor="FerretDB Inc."
+17
View File
@@ -0,0 +1,17 @@
-- Please do not merge changes in this file.
-- See https://pgtap.org/documentation.html#usingpgtap.
-- Start transaction and plan the tests.
BEGIN;
SELECT plan(1); -- number of test assertions
SET documentdb_core.bsonUseEJson = on;
-- Update the following example with your test.
SELECT pass();
-- Finish the tests and clean up.
SELECT * FROM finish();
ROLLBACK;
+2
View File
@@ -15,3 +15,5 @@ scan:
- .env
- .envrc
- build/certs/*.pem
- internal/dataapi/api/openapi.json
- internal/dataapi/api/openapi-minimal.json
@@ -12,11 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !ferretdb_no_postgresql
package version
package main
// init adds "postgresql" backend flags.
func init() {
handlerFlags["postgresql"] = &postgreSQLFlags
}
// DocumentDB is a version of DocumentDB this version of FerretDB is compatible with.
const DocumentDB = "0.100.0 gitref: HEAD sha:ae4b843 buildId:0"
+1 -1
View File
@@ -24,7 +24,7 @@ import (
"strings"
"sync"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// runGit runs `git` with given arguments and returns stdout.
+1 -1
View File
@@ -1 +1 @@
7.0.42
7.0.77
+20 -32
View File
@@ -25,28 +25,18 @@
//
// # Go build tags
//
// The following Go build tags (also known as build constraints) affect all builds of FerretDB,
// including embedded usage:
// The following Go build tags (also known as build constraints) affect builds of FerretDB:
//
// ferretdb_debug - enables debug build (see below; implied by builds with race detector)
// ferretdb_hana - enables Hana backend (alpha)
// ferretdb_no_postgresql - disables PostgreSQL backend
// ferretdb_no_sqlite - disables SQLite backend
// ferretdb_dev - enables development build (see below; implied by builds with race detector)
//
// # Debug builds
// # Development builds
//
// Debug builds of FerretDB behave differently in a few aspects:
// Development builds of FerretDB behave differently in a few aspects:
// - Some values that are normally randomized are fixed or less randomized to make debugging easier.
// - Some internal errors cause crashes instead of being handled more gracefully.
// - Stack traces are collected more liberally.
// - Metrics are written to stderr on exit.
// - The default logging level is set to debug.
//
// Debug builds are orthogonal to production releases, development releases, and local/host builds.
// For example, the host build could be made non-debug, and the production release, in theory, could be a debug build.
//
// Currently, our production releases are non-debug builds,
// and all other builds and releases (development releases, all-in-one releases, local builds, etc.) are debug builds.
package version
import (
@@ -57,13 +47,12 @@ import (
"strconv"
"strings"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/debugbuild"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/devbuild"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// Each pattern in a //go:embed line must match at least one file or non-empty directory,
// but most files are generated and are not present when embeddable FerretDB package is used.
// but most files are generated and may be absent.
// As a workaround, mongodb.txt is always present.
//go:generate go run ./generate.go
@@ -80,14 +69,14 @@ type Info struct {
Branch string
Dirty bool
Package string
DebugBuild bool
BuildEnvironment *types.Document
DevBuild bool
BuildEnvironment map[string]string
// MongoDBVersion is fake MongoDB version for clients that check major.minor to adjust their behavior.
MongoDBVersion string
// MongoDBVersionArray is MongoDBVersion, but as an array.
MongoDBVersionArray *types.Array
MongoDBVersionArray [4]int32
}
// info singleton instance set by init().
@@ -112,7 +101,7 @@ func init() {
minor := must.NotFail(strconv.ParseInt(parts[2], 10, 32))
patch := must.NotFail(strconv.ParseInt(parts[3], 10, 32))
mongoDBVersion := fmt.Sprintf("%d.%d.%d", major, minor, patch)
mongoDBVersionArray := must.NotFail(types.NewArray(int32(major), int32(minor), int32(patch), int32(0)))
mongoDBVersionArray := [...]int32{int32(major), int32(minor), int32(patch), int32(0)}
info = &Info{
Version: unknown,
@@ -120,22 +109,19 @@ func init() {
Branch: unknown,
Dirty: false,
Package: unknown,
DebugBuild: debugbuild.Enabled,
BuildEnvironment: must.NotFail(types.NewDocument()),
DevBuild: devbuild.Enabled,
BuildEnvironment: map[string]string{},
MongoDBVersion: mongoDBVersion,
MongoDBVersionArray: mongoDBVersionArray,
}
// do not expose extra information when embeddable FerretDB package is used
// (and some of it is most likely absent anyway)
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return
}
// TODO https://github.com/FerretDB/FerretDB/issues/4001
if buildInfo.Main.Path != "github.com/FerretDB/FerretDB" {
// for tests
if buildInfo.Main.Path == "" {
return
}
@@ -154,11 +140,11 @@ func init() {
msg := "Invalid build/version/version.txt file content. Please run `bin/task gen-version`.\n"
msg += "Alternatively, create this file manually with a content similar to\n"
msg += "the output of `git describe`: `v<major>.<minor>.<patch>`.\n"
msg += "See https://pkg.go.dev/github.com/FerretDB/FerretDB/build/version"
msg += "See https://pkg.go.dev/github.com/FerretDB/FerretDB/v2/build/version"
panic(msg)
}
info.BuildEnvironment.Set("go.version", buildInfo.GoVersion)
info.BuildEnvironment["go.version"] = buildInfo.GoVersion
for _, s := range buildInfo.Settings {
switch s.Key {
@@ -179,7 +165,9 @@ func init() {
info.Dirty = must.NotFail(strconv.ParseBool(s.Value))
default:
info.BuildEnvironment.Set(s.Key, s.Value)
if v := s.Value; v != "" {
info.BuildEnvironment[s.Key] = v
}
}
}
}
+2 -6
View File
@@ -18,10 +18,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/testutil"
)
func TestGet(t *testing.T) {
@@ -29,6 +25,6 @@ func TestGet(t *testing.T) {
assert.NotEmpty(t, v.Version)
assert.Equal(t, "7.0.42", v.MongoDBVersion)
testutil.AssertEqual(t, must.NotFail(types.NewArray(int32(7), int32(0), int32(42), int32(0))), v.MongoDBVersionArray)
assert.Equal(t, "7.0.77", v.MongoDBVersion)
assert.Equal(t, [...]int32{int32(7), int32(0), int32(77), int32(0)}, v.MongoDBVersionArray)
}
+16 -243
View File
@@ -23,8 +23,6 @@ import (
"html/template"
"io"
"log/slog"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
@@ -34,16 +32,10 @@ import (
"time"
"github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus"
"github.com/FerretDB/FerretDB/build/version"
mysqlpool "github.com/FerretDB/FerretDB/internal/backends/mysql/metadata/pool"
"github.com/FerretDB/FerretDB/internal/backends/postgresql/metadata/pool"
"github.com/FerretDB/FerretDB/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/internal/util/debug"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/logging"
"github.com/FerretDB/FerretDB/internal/util/state"
"github.com/FerretDB/FerretDB/v2/build/version"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
)
var (
@@ -57,225 +49,6 @@ var (
// versionFile contains version information with leading v.
const versionFile = "build/version/version.txt"
// waitForPort waits for the given port to be available until ctx is canceled.
func waitForPort(ctx context.Context, logger *slog.Logger, port uint16) error {
addr := fmt.Sprintf("127.0.0.1:%d", port)
logger.InfoContext(ctx, fmt.Sprintf("Waiting for %s to be up", addr))
var retry int64
for ctx.Err() == nil {
conn, err := net.Dial("tcp", addr)
if err == nil {
conn.Close()
return nil
}
logger.InfoContext(ctx, fmt.Sprintf("%s: %s", addr, err))
retry++
ctxutil.SleepWithJitter(ctx, time.Second, retry)
}
return fmt.Errorf("failed to connect to %s", addr)
}
// setupAnyPostgres configures given PostgreSQL.
func setupAnyPostgres(ctx context.Context, logger *slog.Logger, uri string) error {
u, err := url.Parse(uri)
if err != nil {
return err
}
port, err := strconv.ParseUint(u.Port(), 10, 16)
if err != nil {
return err
}
if u.User == nil {
return lazyerrors.New("No username specified")
}
if err = waitForPort(ctx, logger, uint16(port)); err != nil {
return err
}
sp, err := state.NewProvider("")
if err != nil {
return err
}
p, err := pool.New(uri, logger, sp)
if err != nil {
return err
}
defer p.Close()
username := u.User.Username()
password, _ := u.User.Password()
var retry int64
for ctx.Err() == nil {
if _, err = p.Get(username, password); err == nil {
break
}
logger.InfoContext(ctx, fmt.Sprintf("%s: %s", uri, err))
retry++
ctxutil.SleepWithJitter(ctx, time.Second, retry)
}
if ctx.Err() != nil {
return ctx.Err()
}
return nil
}
// setupPostgres configures `postgres` container.
func setupPostgres(ctx context.Context, logger *slog.Logger) error {
// user `username` must exist, but password may be any, even empty
return setupAnyPostgres(ctx, logging.WithName(logger, "postgres"), "postgres://username@127.0.0.1:5432/ferretdb")
}
// setupPostgresSecured configures `postgres_secured` container.
func setupPostgresSecured(ctx context.Context, logger *slog.Logger) error {
return setupAnyPostgres(ctx, logging.WithName(logger, "postgres_secured"), "postgres://username:password@127.0.0.1:5433/ferretdb") //nolint:lll // for readability
}
// setupMySQL configures `mysql` container.
func setupMySQL(ctx context.Context, logger *slog.Logger) error {
uri := "mysql://root:password@127.0.0.1:3306/ferretdb"
sp, err := state.NewProvider("")
if err != nil {
return err
}
if err = waitForPort(ctx, logging.WithName(logger, "mysql"), 3306); err != nil {
return err
}
p, err := mysqlpool.New(uri, logger, sp)
if err != nil {
return err
}
defer p.Close()
var retry int64
for ctx.Err() == nil {
db, err := p.Get("root", "password")
if err == nil {
if _, err = db.ExecContext(ctx, "GRANT ALL PRIVILEGES ON *.* TO 'username'@'%';"); err != nil {
return lazyerrors.Error(err)
}
break
}
logger.InfoContext(ctx, fmt.Sprintf("%s: %s", uri, err))
retry++
ctxutil.SleepWithJitter(ctx, time.Second, retry)
}
if ctx.Err() != nil {
return ctx.Err()
}
return nil
}
// setupMongodb configures `mongodb` container.
func setupMongodb(ctx context.Context, logger *slog.Logger) error {
if err := waitForPort(ctx, logging.WithName(logger, "mongodb"), 47017); err != nil {
return err
}
eval := `'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:47017" }]})'`
args := []string{"compose", "exec", "-T", "mongodb", "mongosh", "--port=47017", "--eval", eval}
var buf bytes.Buffer
var retry int64
for ctx.Err() == nil {
buf.Reset()
err := runCommand("docker", args, &buf, logger)
if err == nil {
break
}
logger.InfoContext(ctx, fmt.Sprintf("%s:\n%s", err, buf.String()))
retry++
ctxutil.SleepWithJitter(ctx, time.Second, retry)
}
return ctx.Err()
}
// setupMongodbSecured configures `mongodb_secured` container.
func setupMongodbSecured(ctx context.Context, logger *slog.Logger) error {
if err := waitForPort(ctx, logging.WithName(logger, "mongodb_secured"), 47018); err != nil {
return err
}
eval := `'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:47018" }]})'`
shell := `mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=/etc/certs/client.pem&tlsCaFile=/etc/certs/rootCA-cert.pem` //nolint:lll // for readability
args := []string{"compose", "exec", "-T", "mongodb_secured", "mongosh", "--eval", eval, "--shell", shell}
var buf bytes.Buffer
var retry int64
for ctx.Err() == nil {
buf.Reset()
err := runCommand("docker", args, &buf, logger)
if err == nil {
break
}
logger.InfoContext(ctx, fmt.Sprintf("%s:\n%s", err, buf.String()))
retry++
ctxutil.SleepWithJitter(ctx, time.Second, retry)
}
return ctx.Err()
}
// setup runs all setup commands.
func setup(ctx context.Context, logger *slog.Logger) error {
h, err := debug.Listen(&debug.ListenOpts{
TCPAddr: "127.0.0.1:8089",
L: logging.WithName(logger, "debug"),
R: prometheus.DefaultRegisterer,
})
if err != nil {
return lazyerrors.Error(err)
}
go h.Serve(ctx)
for _, f := range []func(context.Context, *slog.Logger) error{
setupPostgres,
setupPostgresSecured,
setupMySQL,
setupMongodb,
setupMongodbSecured,
} {
if err := f(ctx, logger); err != nil {
return err
}
}
logger.InfoContext(ctx, "Done")
return nil
}
// runCommand runs command with given arguments.
func runCommand(command string, args []string, stdout io.Writer, logger *slog.Logger) error {
bin, err := exec.LookPath(command)
@@ -338,12 +111,12 @@ func printDiagnosticData(w io.Writer, setupError error, logger *slog.Logger) err
"GOOS": runtime.GOOS,
"GOARCH": runtime.GOARCH,
"Version": info.Version,
"Commit": info.Commit,
"Branch": info.Branch,
"Dirty": info.Dirty,
"Package": info.Package,
"DebugBuild": info.DebugBuild,
"Version": info.Version,
"Commit": info.Commit,
"Branch": info.Branch,
"Dirty": info.Dirty,
"Package": info.Package,
"DevBuild": info.DevBuild,
"GoVersion": runtime.Version(),
"GitVersion": strings.TrimSpace(gitVersion),
@@ -417,6 +190,7 @@ type TestsRunParams struct {
ShardTotal uint `help:"Total number of shards."`
Run string `help:"Run only tests matching the regexp."`
Skip string `help:"Skip tests matching the regexp."`
RawPrefix string `help:"Prefix for raw output files."`
Args []string `arg:"" help:"Other arguments and flags for 'go test'." passthrough:""`
}
@@ -434,13 +208,13 @@ var cli struct {
Shell struct {
Mkdir struct {
Paths []string `arg:"" name:"path" help:"Paths to create." type:"path"`
Paths []string `name:"path" arg:"" help:"Paths to create." type:"path"`
} `cmd:"" help:"Create directories if they do not already exist."`
Rmdir struct {
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
Paths []string `name:"path" arg:"" help:"Paths to remove." type:"path"`
} `cmd:"" help:"Remove directories."`
Read struct {
Paths []string `arg:"" name:"path" help:"Paths to read." type:"path"`
Paths []string `name:"path" arg:"" help:"Paths to read." type:"path"`
} `cmd:"" help:"Read files."`
} `cmd:""`
@@ -457,10 +231,10 @@ var cli struct {
}
func main() {
kongCtx := kong.Parse(&cli)
kongCtx := kong.Parse(&cli, kong.DefaultEnvars("ENVTOOL"))
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
if t, _ := strconv.ParseBool(os.Getenv("CI")); t {
if t, _ := strconv.ParseBool(os.Getenv("RUNNER_DEBUG")); t {
cli.Debug = true
}
@@ -475,7 +249,6 @@ func main() {
RemoveTime: true,
RemoveSource: true,
}
logging.Setup(opts, "")
logger := slog.Default()
@@ -562,7 +335,7 @@ func main() {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
logger.Error("Failed to exit", logging.Error(exitErr))
logger.Error("Failed", logging.Error(exitErr))
os.Exit(exitErr.ExitCode())
}
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
)
func TestPrintDiagnosticData(t *testing.T) {
+1 -1
View File
@@ -7,7 +7,7 @@ Version: {{.Version}}
Commit: {{.Commit}}
Branch: {{.Branch}}
Dirty: {{.Dirty}}
Debug: {{.DebugBuild}}
Dev: {{.DevBuild}}
{{.GoVersion}}
{{.GitVersion}}
+3 -3
View File
@@ -28,7 +28,7 @@ import (
"slices"
"strings"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
)
// copyFile copies a file from src to dst, overwriting dst if it exists.
@@ -68,7 +68,7 @@ func copyFile(src, dst string) error {
return nil
}
// fuzzGeneratedCorpus returns $GOCACHE/fuzz/github.com/FerretDB/FerretDB,
// fuzzGeneratedCorpus returns $GOCACHE/fuzz/github.com/FerretDB/FerretDB/v2,
// ensuring that this directory exists.
func fuzzGeneratedCorpus() (string, error) {
b, err := exec.Command("go", "env", "GOCACHE").Output()
@@ -76,7 +76,7 @@ func fuzzGeneratedCorpus() (string, error) {
return "", lazyerrors.Error(err)
}
path := filepath.Join(string(bytes.TrimSpace(b)), "fuzz", "github.com", "FerretDB", "FerretDB")
path := filepath.Join(string(bytes.TrimSpace(b)), "fuzz", "github.com", "FerretDB", "FerretDB", "v2")
if _, err = os.Stat(path); err != nil {
if errors.Is(err, fs.ErrNotExist) {
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package main
import (
"bytes"
"context"
"fmt"
"log/slog"
"net"
"net/url"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/debug"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
)
// setupMongoDB configures MongoDB containers.
func setupMongoDB(ctx context.Context, logger *slog.Logger, uri, name string) error {
u, err := url.Parse(uri)
if err != nil {
return lazyerrors.Error(err)
}
_, port, err := net.SplitHostPort(u.Host)
if err != nil {
return lazyerrors.Error(err)
}
logger = logging.WithName(logger, name)
eval := fmt.Sprintf(`'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:%s" }]})'`, port)
args := []string{"compose", "exec", "-T", name, "mongosh", "--eval", eval, uri}
var buf bytes.Buffer
var attempt int64
for ctx.Err() == nil {
buf.Reset()
err := runCommand("docker", args, &buf, logger)
if err == nil {
break
}
logger.InfoContext(ctx, fmt.Sprintf("%s:\n%s", err, buf.String()))
attempt++
ctxutil.SleepWithJitter(ctx, time.Second, attempt)
}
return ctx.Err()
}
// setup runs all setup commands.
func setup(ctx context.Context, logger *slog.Logger) error {
h, err := debug.Listen(&debug.ListenOpts{
TCPAddr: "127.0.0.1:8089",
L: logging.WithName(logger, "debug"),
R: prometheus.DefaultRegisterer,
})
if err != nil {
return lazyerrors.Error(err)
}
go h.Serve(ctx)
if err = setupMongoDB(ctx, logger, "mongodb://username:password@127.0.0.1:47017/", "mongodb-secure"); err != nil {
return lazyerrors.Error(err)
}
logger.InfoContext(ctx, "Done.")
return nil
}
+173 -64
View File
@@ -23,6 +23,7 @@ import (
"fmt"
"io"
"log/slog"
"maps"
"os"
"os/exec"
"slices"
@@ -34,11 +35,10 @@ import (
otelattribute "go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
oteltrace "go.opentelemetry.io/otel/trace"
"golang.org/x/exp/maps"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/observability"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
)
// testEvent represents a single even emitted by `go test -json`.
@@ -69,15 +69,18 @@ type testResult struct {
// listTestFuncs returns a sorted slice of all top-level test functions (tests, benchmarks, examples, fuzz functions)
// matching given regular expression in the specified directory and subdirectories.
func listTestFuncs(dir, re string, logger *slog.Logger) ([]string, error) {
func listTestFuncs(ctx context.Context, dir, re string, logger *slog.Logger) ([]string, error) {
ctx, span := otel.Tracer("").Start(ctx, "listTestFuncs")
defer span.End()
var buf bytes.Buffer
cmd := exec.Command("go", "test", "-list="+re, "./...")
cmd := exec.CommandContext(ctx, "go", "test", "-list="+re, "./...")
cmd.Dir = dir
cmd.Stdout = &buf
cmd.Stderr = os.Stderr
logger.Info(fmt.Sprintf("Running %s", strings.Join(cmd.Args, " ")))
logger.InfoContext(ctx, fmt.Sprintf("Running %s", strings.Join(cmd.Args, " ")))
if err := cmd.Run(); err != nil {
return nil, lazyerrors.Error(err)
@@ -114,10 +117,7 @@ func listTestFuncs(dir, re string, logger *slog.Logger) ([]string, error) {
return nil, lazyerrors.Error(err)
}
res := maps.Keys(testFuncs)
slices.Sort(res)
return res, nil
return slices.Sorted(maps.Keys(testFuncs)), nil
}
// shardTestFuncs shards given top-level test functions.
@@ -155,7 +155,10 @@ func shardTestFuncs(index, total uint, testFuncs []string) ([]string, error) {
}
// testArgs handles `envtool tests run` arguments and returns a slice of `go test` arguments.
func testArgs(dir string, index, total uint, run, skip string, logger *slog.Logger) ([]string, uint, error) {
func testArgs(ctx context.Context, dir string, index, total uint, run, skip string, logger *slog.Logger) ([]string, uint, error) {
ctx, span := otel.Tracer("").Start(ctx, "testArgs")
defer span.End()
listRE := "."
if run != "" || skip != "" {
@@ -181,7 +184,7 @@ func testArgs(dir string, index, total uint, run, skip string, logger *slog.Logg
listRE = run
}
testFuncs, err := listTestFuncs(dir, listRE, logger)
testFuncs, err := listTestFuncs(ctx, dir, listRE, logger)
if err != nil {
return nil, 0, err
}
@@ -216,24 +219,111 @@ func resultKey(packageName, testName string) string {
return packageName + "." + testName
}
// runGoTest runs `go test` with given extra args.
func runGoTest(runCtx context.Context, args []string, total uint, times bool, logger *slog.Logger) error {
cmd := exec.CommandContext(runCtx, "go", append([]string{"test", "-json"}, args...)...)
// startGoTest starts `go test` with given arguments.
func startGoTest(ctx context.Context, args []string, raw string, l *slog.Logger) (*json.Decoder, func() error, error) {
var cleanups []func() error
logger.InfoContext(runCtx, fmt.Sprintf("Running %s", strings.Join(cmd.Args, " ")))
cleanup := func() error {
var err error
for _, f := range slices.Backward(cleanups) {
if e := f(); e != nil {
l.DebugContext(ctx, fmt.Sprintf("Cleanup error: %s", e))
if err == nil {
err = lazyerrors.Error(e)
}
}
}
return err
}
cmd := exec.CommandContext(ctx, "go", append([]string{"test", "-json"}, args...)...)
l.InfoContext(ctx, fmt.Sprintf("Running %s", strings.Join(cmd.Args, " ")))
cmd.Stderr = os.Stderr
p, err := cmd.StdoutPipe()
if err != nil {
return lazyerrors.Error(err)
if raw != "" {
f, e := os.Create(raw + "-stderr.txt.tmp")
if e != nil {
_ = cleanup()
return nil, nil, lazyerrors.Error(e)
}
cleanups = append(cleanups, f.Close)
cmd.Stderr = io.MultiWriter(f, os.Stderr)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
_ = cleanup()
return nil, nil, lazyerrors.Error(err)
}
d := json.NewDecoder(stdout)
if raw != "" {
f, e := os.Create(raw + "-stdout.json.tmp")
if e != nil {
_ = cleanup()
return nil, nil, lazyerrors.Error(e)
}
cleanups = append(cleanups, f.Close)
d = json.NewDecoder(io.TeeReader(stdout, f))
}
d.DisallowUnknownFields()
if err = cmd.Start(); err != nil {
return lazyerrors.Error(err)
_ = cleanup()
return nil, nil, lazyerrors.Error(err)
}
defer cmd.Cancel() //nolint:errcheck // safe to ignore
cleanups = append(cleanups, cmd.Wait)
return d, cleanup, nil
}
// runGoTestOpts represents [runGoTest] options.
//
//nolint:vet // for readability
type runGoTestOpts struct {
args []string
total uint
times bool
rawPrefix string
logger *slog.Logger
}
// runGoTest runs `go test` with given extra args.
func runGoTest(runCtx context.Context, opts *runGoTestOpts) (resErr error) {
totalTests := "?"
if opts.total > 0 {
totalTests = strconv.Itoa(int(opts.total))
}
runCtx, runSpan := otel.Tracer("").Start(runCtx, "run")
runSpan.SetAttributes(otelattribute.String("db.ferretdb.envtool.total_tests", totalTests))
defer runSpan.End()
d, cleanup, err := startGoTest(runCtx, opts.args, opts.rawPrefix, opts.logger)
if err != nil {
resErr = lazyerrors.Error(err)
return
}
defer func() {
if e := cleanup(); resErr == nil && e != nil {
resErr = lazyerrors.Error(e)
}
}()
runSpan.AddEvent("started")
// Keys are:
// - "package/name"
@@ -244,29 +334,24 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
results := make(map[string]*testResult, 300)
var done int
d := json.NewDecoder(p)
d.DisallowUnknownFields()
totalTests := "?"
if total > 0 {
totalTests = strconv.Itoa(int(total))
}
runCtx, runSpan := otel.Tracer("").Start(runCtx, "run")
runSpan.SetAttributes(otelattribute.String("db.ferretdb.envtool.total_tests", totalTests))
defer runSpan.End()
var firstEvent bool
for {
var event testEvent
if err = d.Decode(&event); err != nil {
if !errors.Is(err, io.EOF) {
return lazyerrors.Error(err)
resErr = lazyerrors.Error(err)
return
}
break
}
if !firstEvent {
runSpan.AddEvent("first event")
firstEvent = true
}
must.NotBeZero(event.Package)
key := resultKey(event.Package, event.Test)
@@ -287,7 +372,9 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
// TODO https://github.com/FerretDB/FerretDB/issues/4465
if parent == nil {
panic(fmt.Sprintf("no parent test found: package=%q, test=%q, key=%q", event.Package, event.Test, key))
panic(fmt.Sprintf(
"no parent test found: package=%q, test=%q, key=%q", event.Package, event.Test, key,
))
}
parentCtx = parent.ctx
@@ -337,7 +424,7 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
// initial setup output or early panic
if event.Test == "" {
logger.InfoContext(runCtx, out)
opts.logger.InfoContext(runCtx, out)
continue
}
@@ -362,7 +449,7 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
testSpan.End()
if event.Test == "" {
logger.InfoContext(runCtx, strings.ToTitle(event.Action)+" "+event.Package)
opts.logger.InfoContext(runCtx, strings.ToTitle(event.Action)+" "+event.Package)
continue
}
@@ -373,7 +460,7 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
msg := strings.ToTitle(event.Action) + " " + event.Test
if times {
if opts.times {
msg += fmt.Sprintf(" (%.2fs", event.Time.Sub(res.cont).Seconds())
if res.run != res.cont {
@@ -392,22 +479,29 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
msg += fmt.Sprintf(" %d/%s", done, totalTests)
}
logOutput := opts.logger.WarnContext
if event.Action == "pass" {
logger.InfoContext(runCtx, msg)
continue
if !opts.logger.Enabled(runCtx, slog.LevelDebug) {
opts.logger.InfoContext(runCtx, msg)
continue
}
logOutput = opts.logger.DebugContext
}
msg += ":"
logger.WarnContext(runCtx, msg)
logOutput(runCtx, msg)
for _, l := range res.outputs {
logger.WarnContext(runCtx, l)
logOutput(runCtx, l)
}
logger.WarnContext(runCtx, "")
logOutput(runCtx, "")
default:
return lazyerrors.Errorf("unknown action %q", event.Action)
resErr = lazyerrors.Errorf("unknown action %q", event.Action)
return
}
}
@@ -423,20 +517,20 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
}
if unfinished == nil {
return cmd.Wait()
return
}
slices.Sort(unfinished)
logger.ErrorContext(runCtx, "")
opts.logger.ErrorContext(runCtx, "")
logger.ErrorContext(runCtx, "Some tests did not finish:")
opts.logger.ErrorContext(runCtx, "Some tests did not finish:")
for _, t := range unfinished {
logger.ErrorContext(runCtx, fmt.Sprintf(" %s", t))
opts.logger.ErrorContext(runCtx, fmt.Sprintf(" %s", t))
}
logger.ErrorContext(runCtx, "")
opts.logger.ErrorContext(runCtx, "")
// On panic, the last event will not be "fail"; see https://github.com/golang/go/issues/38382.
// Try to provide the best possible output in that case.
@@ -462,16 +556,16 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
continue
}
logger.ErrorContext(runCtx, fmt.Sprintf("%s:", t))
opts.logger.ErrorContext(runCtx, fmt.Sprintf("%s:", t))
for _, l := range results[t].outputs {
logger.ErrorContext(runCtx, l)
opts.logger.ErrorContext(runCtx, l)
}
logger.ErrorContext(runCtx, "")
opts.logger.ErrorContext(runCtx, "")
}
return cmd.Wait()
return
}
// testsRun runs tests specified by the shard index and total or by the run regex
@@ -479,13 +573,6 @@ func runGoTest(runCtx context.Context, args []string, total uint, times bool, lo
func testsRun(ctx context.Context, params *TestsRunParams, logger *slog.Logger) error {
logger.DebugContext(ctx, fmt.Sprintf("testsRun: %+v", params))
args, total, err := testArgs("", params.ShardIndex, params.ShardTotal, params.Run, params.Skip, logger)
if err != nil {
return lazyerrors.Error(err)
}
args = append(args, params.Args...)
ot, err := observability.NewOTelTraceExporter(&observability.OTelTraceExporterOpts{
Logger: logger,
Service: "envtool-tests",
@@ -495,6 +582,8 @@ func testsRun(ctx context.Context, params *TestsRunParams, logger *slog.Logger)
return lazyerrors.Error(err)
}
ctx, span := otel.Tracer("").Start(ctx, "testsRun")
ctx, cancel := context.WithCancel(ctx)
done := make(chan struct{})
@@ -503,10 +592,30 @@ func testsRun(ctx context.Context, params *TestsRunParams, logger *slog.Logger)
close(done)
}()
err = runGoTest(ctx, args, total, true, logger)
defer func() {
span.End()
cancel()
<-done
}()
cancel()
<-done
args, total, err := testArgs(ctx, "", params.ShardIndex, params.ShardTotal, params.Run, params.Skip, logger)
if err != nil {
return lazyerrors.Error(err)
}
if len(params.Args) > 0 && params.Args[0] == "--" {
params.Args = params.Args[1:]
}
args = append(args, params.Args...)
err = runGoTest(ctx, &runGoTestOpts{
args: args,
total: total,
times: true,
rawPrefix: params.RawPrefix,
logger: logger,
})
return err
}
+56 -31
View File
@@ -16,7 +16,6 @@ package main
import (
"bytes"
"context"
"log/slog"
"os/exec"
"regexp"
@@ -26,8 +25,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/FerretDB/FerretDB/internal/util/logging"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
)
var (
@@ -81,7 +80,9 @@ func bufLogger() (*slog.Logger, *bytes.Buffer) {
func TestTestArgs(t *testing.T) {
t.Parallel()
list, listErr := listTestFuncs("testdata", ".", testutil.Logger(t))
ctx := testutil.Ctx(t)
list, listErr := listTestFuncs(ctx, "testdata", ".", testutil.Logger(t))
require.NoError(t, listErr)
require.Equal(
t,
@@ -92,7 +93,7 @@ func TestTestArgs(t *testing.T) {
t.Run("All", func(t *testing.T) {
t.Parallel()
actual, total, err := testArgs("testdata", 0, 0, "", "", testutil.Logger(t))
actual, total, err := testArgs(ctx, "testdata", 0, 0, "", "", testutil.Logger(t))
require.NoError(t, err)
expected := []string{"-run=^(TestError1|TestError2|TestNormal1|TestNormal2|TestPanic1|TestSkip1|TestWithSubtest)$"}
assert.Equal(t, expected, actual)
@@ -102,7 +103,7 @@ func TestTestArgs(t *testing.T) {
t.Run("Run", func(t *testing.T) {
t.Parallel()
actual, total, err := testArgs("testdata", 0, 0, "(?i)Normal", "", testutil.Logger(t))
actual, total, err := testArgs(ctx, "testdata", 0, 0, "(?i)Normal", "", testutil.Logger(t))
require.NoError(t, err)
expected := []string{"-run=^(TestNormal1|TestNormal2)$"}
assert.Equal(t, expected, actual)
@@ -112,7 +113,7 @@ func TestTestArgs(t *testing.T) {
t.Run("Subtest", func(t *testing.T) {
t.Parallel()
actual, total, err := testArgs("testdata", 0, 0, "TestWithSubtest/Second", "", testutil.Logger(t))
actual, total, err := testArgs(ctx, "testdata", 0, 0, "TestWithSubtest/Second", "", testutil.Logger(t))
require.NoError(t, err)
expected := []string{"-run=TestWithSubtest/Second"}
assert.Equal(t, expected, actual)
@@ -122,7 +123,7 @@ func TestTestArgs(t *testing.T) {
t.Run("Shard", func(t *testing.T) {
t.Parallel()
actual, total, err := testArgs("testdata", 1, 2, "", "", testutil.Logger(t))
actual, total, err := testArgs(ctx, "testdata", 1, 2, "", "", testutil.Logger(t))
require.NoError(t, err)
expected := []string{"-run=^(TestError1|TestNormal1|TestPanic1|TestWithSubtest)$"}
assert.Equal(t, expected, actual)
@@ -133,14 +134,18 @@ func TestTestArgs(t *testing.T) {
func TestRunGoTest(t *testing.T) {
t.Parallel()
ctx := context.TODO()
ctx := testutil.Ctx(t)
t.Run("Normal", func(t *testing.T) {
t.Parallel()
l, buf := bufLogger()
err := runGoTest(ctx, []string{"./testdata", "-count=1", "-run=TestNormal"}, 2, false, l)
err := runGoTest(ctx, &runGoTestOpts{
args: []string{"./testdata", "-count=1", "-run=TestNormal"},
total: 2,
logger: l,
})
require.NoError(t, err)
expected := []string{
@@ -148,8 +153,8 @@ func TestRunGoTest(t *testing.T) {
"INFO PASS TestNormal1 1/2",
"INFO PASS TestNormal2 2/2",
"INFO PASS",
"INFO ok github.com/FerretDB/FerretDB/cmd/envtool/testdata <SEC>s",
"INFO PASS github.com/FerretDB/FerretDB/cmd/envtool/testdata",
"INFO ok github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata <SEC>s",
"INFO PASS github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata",
}
actual := toLogLines(buf)
@@ -161,15 +166,19 @@ func TestRunGoTest(t *testing.T) {
l, buf := bufLogger()
err := runGoTest(ctx, []string{"./testdata", "-count=1", "-run=TestWithSubtest/Third"}, 1, false, l)
err := runGoTest(ctx, &runGoTestOpts{
args: []string{"./testdata", "-count=1", "-run=TestWithSubtest/Third"},
total: 1,
logger: l,
})
require.NoError(t, err)
expected := []string{
"INFO Running go test -json ./testdata -count=1 -run=TestWithSubtest/Third",
"INFO PASS TestWithSubtest 1/1",
"INFO PASS",
"INFO ok github.com/FerretDB/FerretDB/cmd/envtool/testdata <SEC>s",
"INFO PASS github.com/FerretDB/FerretDB/cmd/envtool/testdata",
"INFO ok github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata <SEC>s",
"INFO PASS github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata",
}
actual := toLogLines(buf)
@@ -181,7 +190,11 @@ func TestRunGoTest(t *testing.T) {
l, buf := bufLogger()
err := runGoTest(ctx, []string{"./testdata", "-count=1", "-run=TestWithSubtest/None"}, 1, false, l)
err := runGoTest(ctx, &runGoTestOpts{
args: []string{"./testdata", "-count=1", "-run=TestWithSubtest/None"},
total: 1,
logger: l,
})
require.NoError(t, err)
expected := []string{
@@ -189,8 +202,8 @@ func TestRunGoTest(t *testing.T) {
"INFO PASS TestWithSubtest 1/1",
"INFO testing: warning: no tests to run",
"INFO PASS",
"INFO ok github.com/FerretDB/FerretDB/cmd/envtool/testdata <SEC>s [no tests to run]",
"INFO PASS github.com/FerretDB/FerretDB/cmd/envtool/testdata",
"INFO ok github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata <SEC>s [no tests to run]",
"INFO PASS github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata",
}
actual := toLogLines(buf)
@@ -202,7 +215,11 @@ func TestRunGoTest(t *testing.T) {
l, buf := bufLogger()
err := runGoTest(ctx, []string{"./testdata", "-count=1", "-run=TestError"}, 2, false, l)
err := runGoTest(ctx, &runGoTestOpts{
args: []string{"./testdata", "-count=1", "-run=TestError"},
total: 2,
logger: l,
})
var exitErr *exec.ExitError
require.ErrorAs(t, err, &exitErr)
@@ -236,8 +253,8 @@ func TestRunGoTest(t *testing.T) {
"WARN --- FAIL: TestError2 (<SEC>s)",
"WARN ",
"INFO FAIL",
"INFO FAIL github.com/FerretDB/FerretDB/cmd/envtool/testdata <SEC>s",
"INFO FAIL github.com/FerretDB/FerretDB/cmd/envtool/testdata",
"INFO FAIL github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata <SEC>s",
"INFO FAIL github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata",
}
actual := toLogLines(buf)
@@ -249,7 +266,11 @@ func TestRunGoTest(t *testing.T) {
l, buf := bufLogger()
err := runGoTest(ctx, []string{"./testdata", "-count=1", "-run=TestSkip"}, 1, false, l)
err := runGoTest(ctx, &runGoTestOpts{
args: []string{"./testdata", "-count=1", "-run=TestSkip"},
total: 1,
logger: l,
})
require.NoError(t, err)
expected := []string{
@@ -261,8 +282,8 @@ func TestRunGoTest(t *testing.T) {
"WARN --- SKIP: TestSkip1 (<SEC>s)",
"WARN ",
"INFO PASS",
"INFO ok github.com/FerretDB/FerretDB/cmd/envtool/testdata <SEC>s",
"INFO PASS github.com/FerretDB/FerretDB/cmd/envtool/testdata",
"INFO ok github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata <SEC>s",
"INFO PASS github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata",
}
actual := toLogLines(buf)
@@ -274,25 +295,29 @@ func TestRunGoTest(t *testing.T) {
l, buf := bufLogger()
err := runGoTest(ctx, []string{"./testdata", "-count=1", "-run=TestPanic"}, 1, false, l)
err := runGoTest(ctx, &runGoTestOpts{
args: []string{"./testdata", "-count=1", "-run=TestPanic"},
total: 1,
logger: l,
})
require.Error(t, err)
expected := []string{
"INFO Running go test -json ./testdata -count=1 -run=TestPanic",
"INFO FAIL github.com/FerretDB/FerretDB/cmd/envtool/testdata <SEC>s",
"INFO FAIL github.com/FerretDB/FerretDB/cmd/envtool/testdata",
"INFO FAIL github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata <SEC>s",
"INFO FAIL github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata",
"ERROR ",
"ERROR Some tests did not finish:",
"ERROR github.com/FerretDB/FerretDB/cmd/envtool/testdata.TestPanic1",
"ERROR github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata.TestPanic1",
"ERROR ",
"ERROR github.com/FerretDB/FerretDB/cmd/envtool/testdata.TestPanic1:",
"ERROR github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata.TestPanic1:",
"ERROR === RUN TestPanic1",
"ERROR panic: Panic 1",
"ERROR ",
"ERROR goroutine <ID> [running]:",
"ERROR github.com/FerretDB/FerretDB/cmd/envtool/testdata.TestPanic1.func1()",
"ERROR github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata.TestPanic1.func1()",
"ERROR <DIR>/cmd/envtool/testdata/panic_test.go:25 <ADDR>",
"ERROR created by github.com/FerretDB/FerretDB/cmd/envtool/testdata.TestPanic1 in goroutine <ID>",
"ERROR created by github.com/FerretDB/FerretDB/v2/cmd/envtool/testdata.TestPanic1 in goroutine <ID>",
"ERROR <DIR>/cmd/envtool/testdata/panic_test.go:23 <ADDR>",
"ERROR",
}
+151 -212
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"log"
"log/slog"
"math"
"os"
"path/filepath"
"runtime"
@@ -33,19 +34,20 @@ import (
"go.uber.org/automaxprocs/maxprocs"
_ "golang.org/x/crypto/x509roots/fallback" // register root TLS certificates for production Docker image
"github.com/FerretDB/FerretDB/build/version"
"github.com/FerretDB/FerretDB/internal/clientconn"
"github.com/FerretDB/FerretDB/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/internal/handler/registry"
"github.com/FerretDB/FerretDB/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/internal/util/debug"
"github.com/FerretDB/FerretDB/internal/util/debugbuild"
"github.com/FerretDB/FerretDB/internal/util/logging"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/observability"
"github.com/FerretDB/FerretDB/internal/util/password"
"github.com/FerretDB/FerretDB/internal/util/state"
"github.com/FerretDB/FerretDB/internal/util/telemetry"
"github.com/FerretDB/FerretDB/v2/build/version"
"github.com/FerretDB/FerretDB/v2/internal/clientconn"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/dataapi"
"github.com/FerretDB/FerretDB/v2/internal/documentdb"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/debug"
"github.com/FerretDB/FerretDB/v2/internal/util/devbuild"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
"github.com/FerretDB/FerretDB/v2/internal/util/state"
"github.com/FerretDB/FerretDB/v2/internal/util/telemetry"
)
// The cli struct represents all command-line commands, fields and flags.
@@ -57,19 +59,20 @@ var cli struct {
Run struct{} `cmd:"" default:"1" hidden:""`
Ping struct{} `cmd:"" help:"Ping existing FerretDB instance."`
Version bool `default:"false" help:"Print version to stdout and exit." env:"-"`
Handler string `default:"postgresql" help:"${help_handler}"`
Mode string `default:"${default_mode}" help:"${help_mode}" enum:"${enum_mode}"`
Version bool `default:"false" help:"Print version to stdout and exit." env:"-"`
Mode string `default:"${default_mode}" help:"${help_mode}" enum:"${enum_mode}"`
Auth bool `default:"true" help:"Enable authentication (on by default)." negatable:""`
StateDir string `default:"." help:"Process state directory."`
ReplSetName string `default:"" help:"Replica set name."`
Listen struct {
Addr string `default:"127.0.0.1:27017" help:"Listen TCP address."`
Unix string `default:"" help:"Listen Unix domain socket path."`
TLS string `default:"" help:"Listen TLS address."`
Addr string `default:"127.0.0.1:27017" help:"Listen TCP address for MongoDB protocol."`
Unix string `default:"" help:"Listen Unix domain socket path for MongoDB protocol."`
TLS string `default:"" help:"Listen TLS address for MongoDB protocol."`
TLSCertFile string `default:"" help:"TLS cert file path."`
TLSKeyFile string `default:"" help:"TLS key file path."`
TLSCaFile string `default:"" help:"TLS CA file path."`
DataAPIAddr string `default:"" help:"Listen TCP address for HTTP Data API."`
} `embed:"" prefix:"listen-"`
Proxy struct {
@@ -79,17 +82,7 @@ var cli struct {
TLSCaFile string `default:"" help:"Proxy TLS CA file path."`
} `embed:"" prefix:"proxy-"`
DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, profiling, etc."`
// see setCLIPlugins
kong.Plugins
Setup struct {
Database string `default:"" help:"Setup database during backend initialization."`
Username string `default:"" help:"Setup user during backend initialization."`
Password string `default:"" help:"Setup user's password."`
Timeout time.Duration `default:"30s" help:"Setup timeout."`
} `embed:"" prefix:"setup-"`
DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."`
Log struct {
Level string `default:"${default_log_level}" help:"${help_log_level}"`
@@ -97,6 +90,8 @@ var cli struct {
UUID bool `default:"false" help:"Add instance UUID to all log messages." negatable:""`
} `embed:"" prefix:"log-"`
PostgreSQLURL string `name:"postgresql-url" default:"postgres://127.0.0.1:5432/postgres" help:"PostgreSQL URL."`
MetricsUUID bool `default:"false" help:"Add instance UUID to all metrics." negatable:""`
OTel struct {
@@ -105,86 +100,21 @@ var cli struct {
} `embed:"" prefix:"traces-"`
} `embed:"" prefix:"otel-"`
Telemetry telemetry.Flag `default:"undecided" help:"Enable or disable basic telemetry. See https://beacon.ferretdb.com."`
Telemetry telemetry.Flag `default:"undecided" help:"${help_telemetry}"`
Test struct {
RecordsDir string `default:"" help:"Testing: directory for record files."`
DisablePushdown bool `default:"false" help:"Experimental: disable pushdown."`
EnableNestedPushdown bool `default:"false" help:"Experimental: enable pushdown for dot notation."`
CappedCleanup struct {
Interval time.Duration `default:"1m" help:"Experimental: capped collections cleanup interval."`
Percentage uint8 `default:"10" help:"Experimental: percentage of documents to cleanup."`
} `embed:"" prefix:"capped-cleanup-"`
EnableNewAuth bool `default:"false" help:"Experimental: enable new authentication."`
BatchSize int `default:"100" help:"Experimental: maximum insertion batch size."`
MaxBsonObjectSizeMiB int `default:"16" help:"Experimental: maximum BSON object size in MiB."`
Dev struct {
RecordsDir string `hidden:""`
Telemetry struct {
URL string `default:"https://beacon.ferretdb.com/" help:"Telemetry: reporting URL."`
UndecidedDelay time.Duration `default:"1h" help:"Telemetry: delay for undecided state."`
ReportInterval time.Duration `default:"24h" help:"Telemetry: report interval."`
ReportTimeout time.Duration `default:"5s" help:"Telemetry: report timeout."`
Package string `default:"" help:"Telemetry: custom package type."`
URL string `default:"https://beacon.ferretdb.com/" hidden:""`
UndecidedDelay time.Duration `default:"1h" hidden:""`
ReportInterval time.Duration `default:"24h" hidden:""`
Package string `default:"" hidden:""`
} `embed:"" prefix:"telemetry-"`
} `embed:"" prefix:"test-"`
} `embed:"" prefix:"dev-"`
}
// The postgreSQLFlags struct represents flags that are used by the "postgresql" backend.
//
// See main_postgresql.go.
//
//nolint:lll // some tags are long
var postgreSQLFlags struct {
PostgreSQLURL string `name:"postgresql-url" default:"postgres://127.0.0.1:5432/ferretdb" help:"PostgreSQL URL for 'postgresql' handler."`
}
// The sqliteFlags struct represents flags that are used by the "sqlite" backend.
//
// See main_sqlite.go.
var sqliteFlags struct {
SQLiteURL string `name:"sqlite-url" default:"file:data/" help:"SQLite URI (directory) for 'sqlite' handler."`
}
// The mySQLFlags struct represents flags that are used by the "mysql" backend.
//
// See main_mysql.go.
var mySQLFlags struct {
MySQLURL string `name:"mysql-url" default:"mysql://127.0.0.1:3306/ferretdb" help:"MySQL URL for 'mysql' handler" hidden:""`
}
// The hanaFlags struct represents flags that are used by the "hana" handler.
//
// See main_hana.go.
var hanaFlags struct {
HANAURL string `name:"hana-url" help:"SAP HANA URL for 'hana' handler"`
}
// handlerFlags is a map of handler names to their flags.
var handlerFlags = map[string]any{}
// setCLIPlugins adds Kong flags for handlers in the right order.
func setCLIPlugins() {
handlers := registry.Handlers()
if len(handlers) != len(handlerFlags) {
panic("handlers and handlerFlags are not in sync")
}
for _, h := range handlers {
f := handlerFlags[h]
if f == nil {
panic(fmt.Sprintf("handler %q has no flags", h))
}
cli.Plugins = append(cli.Plugins, f)
}
}
// Additional variables for the kong parsers.
// Additional variables for [kong.Parse].
var (
logLevels = []string{
slog.LevelDebug.String(),
@@ -196,9 +126,6 @@ var (
logFormats = []string{"console", "text", "json"}
kongOptions = []kong.Option{
kong.HelpOptions{
Compact: true,
},
kong.Vars{
"default_log_level": defaultLogLevel().String(),
"default_mode": clientconn.AllModes[0],
@@ -206,22 +133,22 @@ var (
"enum_log_format": strings.Join(logFormats, ","),
"enum_mode": strings.Join(clientconn.AllModes, ","),
"help_handler": fmt.Sprintf("Backend handler: '%s'.", strings.Join(registry.Handlers(), "', '")),
"help_log_format": fmt.Sprintf("Log format: '%s'.", strings.Join(logFormats, "', '")),
"help_log_level": fmt.Sprintf("Log level: '%s'.", strings.Join(logLevels, "', '")),
"help_mode": fmt.Sprintf("Operation mode: '%s'.", strings.Join(clientconn.AllModes, "', '")),
"help_telemetry": "Enable or disable basic telemetry reporting. See https://beacon.ferretdb.com.",
},
kong.DefaultEnvars("FERRETDB"),
}
)
func main() {
setCLIPlugins()
ctx := kong.Parse(&cli, kongOptions...)
switch ctx.Command() {
case "run":
run()
case "ping":
logger := setupLogger(cli.Log.Format, "")
checkFlags(logger)
@@ -244,7 +171,7 @@ func main() {
// defaultLogLevel returns the default log level.
func defaultLogLevel() slog.Level {
if version.Get().DebugBuild {
if version.Get().DevBuild {
return slog.LevelDebug
}
@@ -253,13 +180,13 @@ func defaultLogLevel() slog.Level {
// setupState setups state provider.
func setupState() *state.Provider {
var f string
if cli.StateDir == "" || cli.StateDir == "-" {
log.Fatal("State directory must be set.")
}
if dir := cli.StateDir; dir != "" && dir != "-" {
var err error
if f, err = filepath.Abs(filepath.Join(dir, "state.json")); err != nil {
log.Fatalf("Failed to get path for state file: %s.", err)
}
f, err := filepath.Abs(filepath.Join(cli.StateDir, "state.json"))
if err != nil {
log.Fatalf("Failed to get path for state file: %s.", err)
}
sp, err := state.NewProvider(f)
@@ -290,7 +217,7 @@ func setupMetrics(stateProvider *state.Provider) prometheus.Registerer {
return r
}
// setupLogger creates a logger with the level defined from cli.
// setupLogger setups slog logger.
func setupLogger(format string, uuid string) *slog.Logger {
var level slog.Level
if err := level.UnmarshalText([]byte(cli.Log.Level)); err != nil {
@@ -302,28 +229,25 @@ func setupLogger(format string, uuid string) *slog.Logger {
Level: level,
}
logging.Setup(opts, uuid)
logger := slog.Default()
return slog.Default()
return logger
}
// checkFlags checks that CLI flags are not self-contradictory.
func checkFlags(l *slog.Logger) {
func checkFlags(logger *slog.Logger) {
ctx := context.Background()
if cli.Setup.Database != "" && !cli.Test.EnableNewAuth {
l.LogAttrs(ctx, logging.LevelFatal, "--setup-database requires --test-enable-new-auth")
if devbuild.Enabled {
logger.WarnContext(ctx, "This is a development build. The performance will be affected.")
}
if (cli.Setup.Database == "") != (cli.Setup.Username == "") {
l.LogAttrs(ctx, logging.LevelFatal, "--setup-database should be used together with --setup-username")
if logger.Enabled(ctx, slog.LevelDebug) {
logger.WarnContext(ctx, "Debug logging enabled. The performance will be affected.")
}
if cli.Test.DisablePushdown && cli.Test.EnableNestedPushdown {
l.LogAttrs(
ctx,
logging.LevelFatal,
"--test-disable-pushdown and --test-enable-nested-pushdown should not be set at the same time",
)
if !cli.Auth {
logger.WarnContext(ctx, "Authentication is disabled. The server will accept any connection.")
}
}
@@ -339,7 +263,7 @@ func dumpMetrics() {
// run sets up environment based on provided flags and runs FerretDB.
func run() {
// to increase a chance of resource finalizers to spot problems
if debugbuild.Enabled {
if devbuild.Enabled {
defer func() {
runtime.GC()
runtime.GC()
@@ -348,17 +272,17 @@ func run() {
info := version.Get()
if p := cli.Test.Telemetry.Package; p != "" {
if p := cli.Dev.Telemetry.Package; p != "" {
info.Package = p
}
if cli.Version {
fmt.Fprintln(os.Stdout, "version:", info.Version)
fmt.Fprintln(os.Stdout, "commit:", info.Commit)
fmt.Fprintln(os.Stdout, "branch:", info.Branch)
fmt.Fprintln(os.Stdout, "dirty:", info.Dirty)
fmt.Fprintln(os.Stdout, "package:", info.Package)
fmt.Fprintln(os.Stdout, "debugBuild:", info.DebugBuild)
_, _ = fmt.Fprintln(os.Stdout, "version:", info.Version)
_, _ = fmt.Fprintln(os.Stdout, "commit:", info.Commit)
_, _ = fmt.Fprintln(os.Stdout, "branch:", info.Branch)
_, _ = fmt.Fprintln(os.Stdout, "dirty:", info.Dirty)
_, _ = fmt.Fprintln(os.Stdout, "package:", info.Package)
_, _ = fmt.Fprintln(os.Stdout, "devBuild:", info.DevBuild)
return
}
@@ -376,7 +300,7 @@ func run() {
slog.String("branch", info.Branch),
slog.Bool("dirty", info.Dirty),
slog.String("package", info.Package),
slog.Bool("debugBuild", info.DebugBuild),
slog.Bool("devBuild", info.DevBuild),
slog.Any("buildEnvironment", info.BuildEnvironment),
}
logUUID := stateProvider.Get().UUID
@@ -391,19 +315,18 @@ func run() {
logger.LogAttrs(context.Background(), slog.LevelInfo, "Starting FerretDB "+info.Version+"...", startupFields...)
if debugbuild.Enabled {
logger.Info("This is a debug build. The performance will be affected.")
}
if logger.Enabled(context.Background(), slog.LevelDebug) {
logger.Info("Debug logging enabled. The security and performance will be affected.")
}
checkFlags(logger)
if _, err := maxprocs.Set(maxprocs.Logger(func(format string, a ...any) {
logger.Debug(fmt.Sprintf(format, a...))
})); err != nil {
maxprocsOpts := []maxprocs.Option{
maxprocs.Min(2),
maxprocs.RoundQuotaFunc(func(v float64) int {
return int(math.Ceil(v))
}),
maxprocs.Logger(func(format string, a ...any) {
logger.Info(fmt.Sprintf(format, a...))
}),
}
if _, err := maxprocs.Set(maxprocsOpts...); err != nil {
logger.Warn("Failed to set GOMAXPROCS", logging.Error(err))
}
@@ -411,7 +334,7 @@ func run() {
go func() {
<-ctx.Done()
logger.Info("Stopping...")
logger.InfoContext(ctx, "Stopping")
// second SIGTERM should immediately stop the process
stop()
@@ -444,6 +367,7 @@ func run() {
return listener.Load().Listening()
},
Readyz: ready.Probe,
})
if err != nil {
@@ -478,70 +402,64 @@ func run() {
metrics := connmetrics.NewListenerMetrics()
wg.Add(1)
{
wg.Add(1)
go func() {
defer wg.Done()
go func() {
defer wg.Done()
l := logging.WithName(logger, "telemetry")
opts := &telemetry.NewReporterOpts{
URL: cli.Test.Telemetry.URL,
F: &cli.Telemetry,
DNT: os.Getenv("DO_NOT_TRACK"),
ExecName: os.Args[0],
P: stateProvider,
ConnMetrics: metrics.ConnMetrics,
L: l,
UndecidedDelay: cli.Test.Telemetry.UndecidedDelay,
ReportInterval: cli.Test.Telemetry.ReportInterval,
ReportTimeout: cli.Test.Telemetry.ReportTimeout,
}
l := logging.WithName(logger, "telemetry")
r, err := telemetry.NewReporter(opts)
if err != nil {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to create telemetry reporter", logging.Error(err))
}
file, err := filepath.Abs(filepath.Join(cli.StateDir, "telemetry.json"))
if err != nil {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to get path for local telemetry report file", logging.Error(err))
}
r.Run(ctx)
}()
r, err := telemetry.NewReporter(&telemetry.NewReporterOpts{
URL: cli.Dev.Telemetry.URL,
File: file,
F: &cli.Telemetry,
DNT: os.Getenv("DO_NOT_TRACK"),
ExecName: os.Args[0],
P: stateProvider,
ConnMetrics: metrics.ConnMetrics,
L: l,
UndecidedDelay: cli.Dev.Telemetry.UndecidedDelay,
ReportInterval: cli.Dev.Telemetry.ReportInterval,
})
if err != nil {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to create telemetry reporter", logging.Error(err))
}
h, closeBackend, err := registry.NewHandler(cli.Handler, &registry.NewHandlerOpts{
Logger: logger,
ConnMetrics: metrics.ConnMetrics,
StateProvider: stateProvider,
TCPHost: cli.Listen.Addr,
ReplSetName: cli.ReplSetName,
SetupDatabase: cli.Setup.Database,
SetupUsername: cli.Setup.Username,
SetupPassword: password.WrapPassword(cli.Setup.Password),
SetupTimeout: cli.Setup.Timeout,
PostgreSQLURL: postgreSQLFlags.PostgreSQLURL,
SQLiteURL: sqliteFlags.SQLiteURL,
HANAURL: hanaFlags.HANAURL,
MySQLURL: mySQLFlags.MySQLURL,
TestOpts: registry.TestOpts{
DisablePushdown: cli.Test.DisablePushdown,
EnableNestedPushdown: cli.Test.EnableNestedPushdown,
CappedCleanupInterval: cli.Test.CappedCleanup.Interval,
CappedCleanupPercentage: cli.Test.CappedCleanup.Percentage,
EnableNewAuth: cli.Test.EnableNewAuth,
BatchSize: cli.Test.BatchSize,
MaxBsonObjectSizeBytes: cli.Test.MaxBsonObjectSizeMiB * 1024 * 1024,
},
})
if err != nil {
logger.LogAttrs(ctx, logging.LevelFatal, "Failed to construct handler", logging.Error(err))
r.Run(ctx)
}()
}
defer closeBackend()
p, err := documentdb.NewPool(cli.PostgreSQLURL, logging.WithName(logger, "pool"), stateProvider)
if err != nil {
logger.LogAttrs(ctx, logging.LevelFatal, "Failed to construct pool", logging.Error(err))
}
l, err := clientconn.Listen(&clientconn.NewListenerOpts{
defer p.Close()
handlerOpts := &handler.NewOpts{
Pool: p,
Auth: cli.Auth,
TCPHost: cli.Listen.Addr,
ReplSetName: cli.ReplSetName,
L: logging.WithName(logger, "handler"),
ConnMetrics: metrics.ConnMetrics,
StateProvider: stateProvider,
}
h, err := handler.New(handlerOpts)
if err != nil {
handlerOpts.L.LogAttrs(ctx, logging.LevelFatal, "Failed to construct handler", logging.Error(err))
}
lis, err := clientconn.Listen(&clientconn.NewListenerOpts{
TCP: cli.Listen.Addr,
Unix: cli.Listen.Unix,
@@ -559,23 +477,44 @@ func run() {
Metrics: metrics,
Handler: h,
Logger: logger,
TestRecordsDir: cli.Test.RecordsDir,
TestRecordsDir: cli.Dev.RecordsDir,
})
if err != nil {
logger.LogAttrs(ctx, logging.LevelFatal, "Failed to construct listener", logging.Error(err))
}
listener.Store(l)
if addr := cli.Listen.DataAPIAddr; addr != "" && addr != "-" {
wg.Add(1)
metricsRegisterer.MustRegister(l)
go func() {
defer wg.Done()
l.Run(ctx)
l := logging.WithName(logger, "dataapi")
logger.Info("Listener stopped")
var lis *dataapi.Listener
lis, err = dataapi.Listen(&dataapi.ListenOpts{
TCPAddr: addr,
L: l,
Handler: h,
})
if err != nil {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to construct DataAPI listener", logging.Error(err))
}
lis.Run(ctx)
}()
}
listener.Store(lis)
metricsRegisterer.MustRegister(lis)
lis.Run(ctx)
wg.Wait()
if info.DebugBuild {
if info.DevBuild {
dumpMetrics()
}
}
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
//go:build ferretdb_hana
package main
// init adds "hana" handler flags when "ferretdb_hana" build tag is provided.
func init() {
handlerFlags["hana"] = &hanaFlags
}
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
//go:build !ferretdb_no_sqlite
package main
// init adds "sqlite" backend flags.
func init() {
handlerFlags["sqlite"] = &sqliteFlags
}
@@ -16,6 +16,7 @@ package main
import (
"context"
"encoding/json"
"os/exec"
"path/filepath"
"regexp"
@@ -25,7 +26,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
)
func TestVersion(t *testing.T) {
@@ -38,7 +39,7 @@ func TestVersion(t *testing.T) {
ctx, cancel := context.WithTimeout(testutil.Ctx(t), 5*time.Second)
t.Cleanup(cancel)
bin := filepath.Join(testutil.BinDir, "ferretdb")
bin := filepath.Join("..", "..", "bin", "ferretdb")
cmd := exec.CommandContext(ctx, bin, "--version")
b, err := cmd.Output()
@@ -57,3 +58,16 @@ func TestVersion(t *testing.T) {
assert.Equal(t, commit[1], revision[1])
}
func TestDeps(t *testing.T) {
t.Parallel()
var res struct {
Deps []string `json:"Deps"`
}
b, err := exec.Command("go", "list", "-json").Output()
require.NoError(t, err)
require.NoError(t, json.Unmarshal(b, &res))
assert.NotContains(t, res.Deps, "testing", `package "testing" should not be imported by non-testing code`)
}
+3 -2
View File
@@ -21,7 +21,8 @@ import (
"os"
"os/user"
"strconv"
"syscall"
"golang.org/x/sys/unix"
)
// stateFileProblem adds details to the state file access error.
@@ -41,7 +42,7 @@ func stateFileProblem(f string, err error) string {
var username, group string
var uid, gid uint64
if s, _ := fi.Sys().(*syscall.Stat_t); s != nil {
if s, _ := fi.Sys().(*unix.Stat_t); s != nil {
uid = uint64(s.Uid)
if u, _ := user.LookupId(strconv.FormatUint(uid, 10)); u != nil {
username = u.Username
-127
View File
@@ -1,127 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package main
import (
"context"
"fmt"
"log/slog"
"net"
"net/url"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/FerretDB/FerretDB/internal/util/logging"
)
// ReadyZ represents the Readiness probe, which is used to run `ping`
// command against the FerretDB instance specified by cli flags.
type ReadyZ struct {
l *slog.Logger
}
// Probe executes ping queries to open listeners, and returns true if they succeed.
// Any errors that occure are passed through ReadyZ.l listener.
//
// It is only executed if --setup-database flag is set.
func (ready *ReadyZ) Probe(ctx context.Context) bool {
l := ready.l
if cli.Setup.Database == "" {
l.InfoContext(ctx, "Setup database not specified - skipping ping")
return true
}
var urls []string
if cli.Listen.Addr != "" {
host, port, err := net.SplitHostPort(cli.Listen.Addr)
if err != nil {
l.ErrorContext(ctx, "Getting host and port failed", logging.Error(err))
return false
}
l.DebugContext(ctx, fmt.Sprintf("--listen-addr flag is set. Ping to %s will be performed", cli.Listen.Addr))
if host == "" {
host = "127.0.0.1"
l.DebugContext(ctx, fmt.Sprintf("Host not specified, defaulting to %s", host))
}
u := &url.URL{
Scheme: "mongodb",
Host: net.JoinHostPort(host, port),
Path: cli.Setup.Database,
User: url.UserPassword(cli.Setup.Username, cli.Setup.Password),
}
urls = append(urls, u.String())
}
if cli.Listen.TLS != "" {
// TODO https://github.com/FerretDB/FerretDB/issues/4427
l.WarnContext(ctx, "TLS ping is not implemented yet")
}
if cli.Listen.Unix != "" {
l.DebugContext(ctx, fmt.Sprintf("--listen-unix flag is set. Ping to %s will be performed", cli.Listen.Unix))
urls = append(urls, "mongodb://"+url.PathEscape(cli.Listen.Unix))
}
if len(urls) == 0 {
l.InfoContext(ctx, "Neither --listen-addr nor --listen-unix nor --listen-tls flags were specified - skipping ping")
return true
}
for _, u := range urls {
l.DebugContext(ctx, fmt.Sprintf("Pinging %s", u))
var cancel func()
ctx, cancel = context.WithTimeout(ctx, cli.Setup.Timeout)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(u))
if err != nil {
l.ErrorContext(ctx, "Connection failed", logging.Error(err))
return false
}
pingErr := client.Ping(ctx, nil)
// do not leave connection open when ping error causes os.Exit with Fatal
if err = client.Disconnect(ctx); err != nil {
l.ErrorContext(ctx, "Disconnect failed", logging.Error(err))
return false
}
if pingErr != nil {
l.ErrorContext(ctx, "Ping failed", logging.Error(pingErr))
return false
}
var uri *url.URL
if uri, err = url.Parse(u); err == nil {
u = uri.Redacted()
}
l.InfoContext(ctx, fmt.Sprintf("Ping to %s successful", u))
}
return true
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package main
import (
"context"
"fmt"
"log/slog"
"net"
"net/url"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
)
// ReadyZ represents the Readiness probe.
//
// It connects to FerretDB with Go driver and sends `ping` command.
type ReadyZ struct {
l *slog.Logger
}
// Probe implements [debug.Probe].
func (r *ReadyZ) Probe(ctx context.Context) bool {
var urls []string
if cli.Listen.Addr != "" {
host, port, err := net.SplitHostPort(cli.Listen.Addr)
if err != nil {
r.l.ErrorContext(ctx, "Getting host and port failed", logging.Error(err))
return false
}
if host == "" {
host = "127.0.0.1"
}
u := &url.URL{
Scheme: "mongodb",
Host: net.JoinHostPort(host, port),
Path: "/",
}
urls = append(urls, u.String())
}
if cli.Listen.TLS != "" {
// TODO https://github.com/FerretDB/FerretDB/issues/4427
r.l.WarnContext(ctx, "TLS ping is not implemented yet")
}
if cli.Listen.Unix != "" {
urls = append(urls, "mongodb://"+url.PathEscape(cli.Listen.Unix))
}
if len(urls) == 0 {
r.l.InfoContext(ctx, "Nothing to ping")
return true
}
for _, u := range urls {
r.l.DebugContext(ctx, fmt.Sprintf("Pinging %s", u))
client, err := mongo.Connect(ctx, options.Client().ApplyURI(u))
if err != nil {
r.l.ErrorContext(ctx, "Connection failed", logging.Error(err))
return false
}
pingErr := client.Ping(ctx, nil)
err = client.Disconnect(ctx)
if pingErr != nil {
r.l.ErrorContext(ctx, "Ping failed", logging.Error(pingErr))
return false
}
if err != nil {
r.l.ErrorContext(ctx, "Disconnect failed", logging.Error(err))
return false
}
r.l.InfoContext(ctx, fmt.Sprintf("Ping to %s successful", u))
}
return true
}
+33 -66
View File
@@ -5,8 +5,9 @@ services:
postgres:
build:
context: ./build/deps
dockerfile: ${POSTGRES_DOCKERFILE:-postgres}.Dockerfile
dockerfile: postgres-documentdb.Dockerfile
container_name: ferretdb_postgres
platform: linux/amd64
command: >
postgres
-c log_min_duration_statement=1000ms
@@ -22,88 +23,54 @@ services:
- TZ=America/St_Johns
- POSTGRES_USER=username
- POSTGRES_PASSWORD=password
- POSTGRES_HOST_AUTH_METHOD=trust
- POSTGRES_DB=ferretdb
postgres_secured:
build:
context: ./build/deps
dockerfile: postgres.Dockerfile
container_name: ferretdb_postgres_secured
command: >
postgres
-c log_min_duration_statement=1000ms
-c log_min_error_statement=WARNING
-c log_min_messages=WARNING
-c max_connections=400
ports:
- 5433:5432
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# UTC−03:30/−02:30. Set to catch timezone problems.
- TZ=America/St_Johns
- POSTGRES_USER=username
- POSTGRES_PASSWORD=password
- POSTGRES_DB=ferretdb
mysql:
build:
context: ./build/deps
dockerfile: mysql.Dockerfile
container_name: ferretdb_mysql
command:
- --slow_query_log=1
- --long_query_time=1
- --log_error_verbosity=2
- --max_connections=200
ports:
- 3306:3306
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# UTC−03:30/−02:30. Set to catch timezone problems.
- TZ=America/St_Johns
- MYSQL_ROOT_PASSWORD=password
- MYSQL_USER=username
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=ferretdb
- POSTGRES_DB=postgres
volumes:
- ./build/testscripts:/testscripts
mongodb:
build:
context: ./build/deps
dockerfile: mongo.Dockerfile
dockerfile: mongodb.Dockerfile
container_name: ferretdb_mongodb
command: --config /etc/mongodb.conf
ports:
- 37017:37017
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# Always UTC+05:45. Set to catch timezone problems.
- TZ=Asia/Kathmandu
ulimits:
nproc: 65535
nofile:
soft: 40000
hard: 80000
volumes:
- ./build/mongodb.conf:/etc/mongodb.conf
mongodb-secure:
build:
context: ./build/deps
dockerfile: mongodb.Dockerfile
container_name: ferretdb_mongodb_secure
command: --config /etc/mongodb.conf
ports:
- 47017:47017
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# Always UTC+05:45. Set to catch timezone problems.
- TZ=Asia/Kathmandu
volumes:
- ./build/certs:/etc/certs
- ./build/mongodb.conf:/etc/mongodb.conf
mongodb_secured:
build:
context: ./build/deps
dockerfile: mongo.Dockerfile
container_name: ferretdb_mongodb_secured
command: --config /etc/mongodb.conf
ports:
- 47018:47018
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# Always UTC+05:45. Set to catch timezone problems.
- TZ=Asia/Kathmandu
- MONGO_INITDB_ROOT_USERNAME=username
- MONGO_INITDB_ROOT_PASSWORD=password
ulimits:
nproc: 65535
nofile:
soft: 40000
hard: 80000
volumes:
- ./build/certs:/etc/certs
- ./build/mongodb_secured.conf:/etc/mongodb.conf
- ./build/mongodb-secure.conf:/etc/mongodb.conf
# for test scripts
legacy-mongo-shell:
-219
View File
@@ -1,219 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
// Package ferretdb provides embeddable FerretDB implementation.
//
// See [github.com/FerretDB/FerretDB/build/version] package documentation
// for information about Go build tags that affect this package.
package ferretdb
import (
"context"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/FerretDB/FerretDB/build/version"
"github.com/FerretDB/FerretDB/internal/clientconn"
"github.com/FerretDB/FerretDB/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/internal/handler/registry"
"github.com/FerretDB/FerretDB/internal/util/state"
)
// Config represents FerretDB configuration.
type Config struct {
Listener ListenerConfig
// Logger to use; if nil, `slog.Default()` is used.
Logger *slog.Logger
// Handler to use; one of `postgresql` or `sqlite`.
Handler string
// PostgreSQL connection string for `postgresql` handler.
// See:
// - https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#ParseConfig
// - https://pkg.go.dev/github.com/jackc/pgx/v5#ParseConfig
// - https://pkg.go.dev/github.com/jackc/pgx/v5/pgconn#ParseConfig
PostgreSQLURL string // For example: `postgres://hostname:5432/ferretdb`.
// SQLite URI (directory) for `sqlite` handler.
// See https://www.sqlite.org/uri.html.
SQLiteURL string // For example: `file:data/`.
}
// ListenerConfig represents listener configuration.
type ListenerConfig struct {
// Listen TCP address.
// If empty, TCP listener is disabled.
TCP string
// Listen Unix domain socket path.
// If empty, Unix listener is disabled.
Unix string
// Listen TLS address.
// If empty, TLS listener is disabled.
TLS string
// Server certificate path.
TLSCertFile string
// Server key path.
TLSKeyFile string
// Root CA certificate path.
TLSCAFile string
}
// FerretDB represents an instance of embeddable FerretDB implementation.
type FerretDB struct {
config *Config
closeBackend func()
l *clientconn.Listener
}
// New creates a new instance of embeddable FerretDB implementation.
func New(config *Config) (*FerretDB, error) {
version.Get().Package = "embedded"
if config.Listener.TCP == "" &&
config.Listener.Unix == "" &&
config.Listener.TLS == "" {
return nil, errors.New("Listener TCP, Unix and TLS are empty")
}
sp, err := state.NewProvider("")
if err != nil {
return nil, fmt.Errorf("failed to construct handler: %s", err)
}
// Telemetry reporter is not created or running anyway,
// but disable telemetry explicitly to disable confusing startupWarnings.
err = sp.Update(func(s *state.State) {
s.DisableTelemetry()
s.TelemetryLocked = true
})
if err != nil {
return nil, fmt.Errorf("failed to construct handler: %s", err)
}
metrics := connmetrics.NewListenerMetrics()
logger := config.Logger
if logger == nil {
logger = slog.Default()
}
h, closeBackend, err := registry.NewHandler(config.Handler, &registry.NewHandlerOpts{
Logger: logger,
ConnMetrics: metrics.ConnMetrics,
StateProvider: sp,
TCPHost: config.Listener.TCP,
PostgreSQLURL: config.PostgreSQLURL,
SQLiteURL: config.SQLiteURL,
TestOpts: registry.TestOpts{
CappedCleanupPercentage: 10,
BatchSize: 100,
},
})
if err != nil {
if closeBackend != nil {
closeBackend()
}
return nil, fmt.Errorf("failed to construct handler: %s", err)
}
l, err := clientconn.Listen(&clientconn.NewListenerOpts{
TCP: config.Listener.TCP,
Unix: config.Listener.Unix,
TLS: config.Listener.TLS,
TLSCertFile: config.Listener.TLSCertFile,
TLSKeyFile: config.Listener.TLSKeyFile,
TLSCAFile: config.Listener.TLSCAFile,
Mode: clientconn.NormalMode,
Metrics: metrics,
Handler: h,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("failed to construct handler: %s", err)
}
return &FerretDB{
config: config,
closeBackend: closeBackend,
l: l,
}, nil
}
// Run runs FerretDB until ctx is canceled.
//
// When this method returns, listener and all connections, as well as handler are closed.
//
// It is required to run this method in order to initialize the listeners with their respective
// IP address and port. Calling methods which require the listener's address (eg: [*FerretDB.MongoDBURI]
// requires it for configuring its Host URL) before calling this method might result in a deadlock.
func (f *FerretDB) Run(ctx context.Context) error {
defer f.closeBackend()
f.l.Run(ctx)
return nil
}
// MongoDBURI returns MongoDB URI for this FerretDB instance.
//
// TCP's connection string is returned if both TCP and Unix listeners are enabled.
// TLS is preferred over both.
func (f *FerretDB) MongoDBURI() string {
var u *url.URL
switch {
case f.config.Listener.TLS != "":
q := url.Values{
"tls": []string{"true"},
}
u = &url.URL{
Scheme: "mongodb",
Host: f.l.TLSAddr().String(),
Path: "/",
RawQuery: q.Encode(),
}
case f.config.Listener.TCP != "":
u = &url.URL{
Scheme: "mongodb",
Host: f.l.TCPAddr().String(),
Path: "/",
}
case f.config.Listener.Unix != "":
// MongoDB really wants Unix domain socket path in the host part of the URI
u = &url.URL{
Scheme: "mongodb",
Host: f.l.UnixAddr().String(),
Path: "/",
}
}
return u.String()
}
-200
View File
@@ -1,200 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package ferretdb_test
import (
"context"
"fmt"
"log"
"log/slog"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/FerretDB/FerretDB/ferretdb"
"github.com/FerretDB/FerretDB/internal/util/testutil"
)
func Example_tcp() {
f, err := ferretdb.New(&ferretdb.Config{
Listener: ferretdb.ListenerConfig{
TCP: "127.0.0.1:17027",
},
Logger: slog.With("component", "ferretdb"),
Handler: "postgresql",
PostgreSQLURL: "postgres://127.0.0.1:5432/ferretdb",
})
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error)
go func() {
done <- f.Run(ctx)
}()
uri := f.MongoDBURI()
fmt.Println(uri)
// Use MongoDB URI as usual.
// For example:
//
// import "go.mongodb.org/mongo-driver/mongo"
// import "go.mongodb.org/mongo-driver/mongo/options"
//
// [...]
//
// mongo.Connect(ctx, options.Client().ApplyURI(uri))
cancel()
err = <-done
if err != nil {
log.Fatal(err)
}
// Output: mongodb://127.0.0.1:17027/
}
func Example_unix() {
f, err := ferretdb.New(&ferretdb.Config{
Listener: ferretdb.ListenerConfig{
Unix: "/tmp/ferretdb.sock",
},
Logger: slog.With("component", "ferretdb"),
Handler: "postgresql",
PostgreSQLURL: "postgres://127.0.0.1:5432/ferretdb",
})
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error)
go func() {
done <- f.Run(ctx)
}()
uri := f.MongoDBURI()
fmt.Println(uri)
// Use MongoDB URI as usual.
// For example:
//
// import "go.mongodb.org/mongo-driver/mongo"
// import "go.mongodb.org/mongo-driver/mongo/options"
//
// [...]
//
// mongo.Connect(ctx, options.Client().ApplyURI(uri))
cancel()
err = <-done
if err != nil {
log.Fatal(err)
}
// Output: mongodb://%2Ftmp%2Fferretdb.sock/
}
func Example_tls() {
f, err := ferretdb.New(&ferretdb.Config{
Listener: ferretdb.ListenerConfig{
TLS: "127.0.0.1:17028",
TLSCertFile: filepath.Join(testutil.BuildCertsDir, "server-cert.pem"),
TLSKeyFile: filepath.Join(testutil.BuildCertsDir, "server-key.pem"),
TLSCAFile: filepath.Join(testutil.BuildCertsDir, "rootCA-cert.pem"),
},
Logger: slog.With("component", "ferretdb"),
Handler: "postgresql",
PostgreSQLURL: "postgres://127.0.0.1:5432/ferretdb",
})
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error)
go func() {
done <- f.Run(ctx)
}()
uri := f.MongoDBURI()
fmt.Println(uri)
// Use MongoDB URI as usual.
// To connect to TLS listener, set TLS config.
// For example:
//
// import "go.mongodb.org/mongo-driver/mongo"
// import "go.mongodb.org/mongo-driver/mongo/options"
//
// [...]
//
// mongo.Connect(ctx, options.Client().ApplyURI(uri))
cancel()
err = <-done
if err != nil {
log.Fatal(err)
}
// Output: mongodb://127.0.0.1:17028/?tls=true
}
func TestEmbedded(t *testing.T) {
ctx, cancel := context.WithCancel(testutil.Ctx(t))
f, err := ferretdb.New(&ferretdb.Config{
Listener: ferretdb.ListenerConfig{
TCP: "127.0.0.1:0",
},
Logger: testutil.Logger(t),
Handler: "postgresql",
PostgreSQLURL: testutil.TestPostgreSQLURI(t, ctx, ""),
})
require.NoError(t, err)
done := make(chan struct{})
go func() {
require.NoError(t, f.Run(ctx))
close(done)
}()
uri := f.MongoDBURI()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
require.NoError(t, err)
dbName, collName := testutil.DatabaseName(t), testutil.CollectionName(t)
//nolint:forbidigo // bson is required to use the driver
_, err = client.Database(dbName).Collection(collName).InsertOne(ctx, bson.M{"foo": "bar"})
require.NoError(t, err)
cancel()
<-done
}
+32 -43
View File
@@ -1,4 +1,4 @@
module github.com/FerretDB/FerretDB
module github.com/FerretDB/FerretDB/v2
go 1.23
@@ -6,75 +6,64 @@ toolchain go1.23.5
require (
github.com/AlekSi/pointer v1.2.0
github.com/FerretDB/wire v0.0.8
github.com/SAP/go-hdb v1.12.0
github.com/alecthomas/kong v0.9.0
github.com/FerretDB/wire v0.0.16
github.com/alecthomas/kong v1.6.1
github.com/arl/statsviz v0.6.0
github.com/go-sql-driver/mysql v1.8.1
github.com/google/uuid v1.6.0
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.6.0
github.com/jackc/pgx/v5 v5.7.2
github.com/oapi-codegen/runtime v1.1.1
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v1.20.2
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.57.0
github.com/stretchr/testify v1.9.0
github.com/prometheus/common v0.62.0
github.com/stretchr/testify v1.10.0
github.com/xdg-go/scram v1.1.2
github.com/xdg-go/stringprep v1.0.4
go.mongodb.org/mongo-driver v1.16.1
go.opentelemetry.io/otel v1.29.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0
go.opentelemetry.io/otel/sdk v1.29.0
go.opentelemetry.io/otel/trace v1.29.0
go.uber.org/automaxprocs v1.5.3
golang.org/x/crypto v0.32.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20250106144430-8929309228b4
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
go.mongodb.org/mongo-driver v1.17.2
go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0
go.opentelemetry.io/otel/sdk v1.34.0
go.opentelemetry.io/otel/trace v1.34.0
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20250118192723-a8ea4be81f07
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
golang.org/x/sys v0.29.0
modernc.org/sqlite v1.32.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver/v2 v2.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.69.4 // indirect
google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
+72 -103
View File
@@ -1,21 +1,21 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/FerretDB/wire v0.0.8 h1:5kttr1Hd60vWbvllemMcxwqTx7yedVVxxOqsliFdMGw=
github.com/FerretDB/wire v0.0.8/go.mod h1:6y7usTYfOlJc3w3l2R/PcViJjKSqyYQhrKa3aeAoekI=
github.com/SAP/go-hdb v1.12.0 h1:ZSjQqVfOITKMiLN7ycbfRncrNcjzRw9/p349v4oEODI=
github.com/SAP/go-hdb v1.12.0/go.mod h1:0DG4r/GryOydt1Z+ZardSS806ADjty2bR0wCPFpOzZA=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/FerretDB/wire v0.0.16 h1:87gEvXigjuV+4mYXzinqKLGzS1sRlJ8ChCaxxTpzt18=
github.com/FerretDB/wire v0.0.16/go.mod h1:fw7JPAF0YHB/N7nE5yyRrBG1iujIeOYRYv89gFKu6e0=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8=
github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE=
github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -23,41 +23,36 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -66,73 +61,74 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY=
github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8=
go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI=
go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250106144430-8929309228b4 h1:2UmVXDw2sT/v4mRp1tucroUkHyqAbK1SXfh+ss3sKsE=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250106144430-8929309228b4/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250118192723-a8ea4be81f07 h1:Tuk3hxOkRoX4Xwph6/tRU1wGumEsVYM2TZfvAC6MllM=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250118192723-a8ea4be81f07/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -147,7 +143,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -161,46 +156,20 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo=
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+5 -3
View File
@@ -5,6 +5,7 @@
run:
timeout: 3m
# ../bin/golangci-lint linters -c .golangci-new.yml
linters-settings:
asasalint:
exclude:
@@ -18,8 +19,6 @@ linters-settings:
check-blank: false
disable-default-exclusions: false
exclude-functions:
- (*database/sql.Rows).Close
- (*github.com/FerretDB/FerretDB/internal/util/fsql.Rows).Close
- (*go.mongodb.org/mongo-driver/mongo.Cursor).Close
errorlint:
# see caveats at https://github.com/polyfloyd/go-errorlint#fmterrorf-wrapping-verb
@@ -78,6 +77,8 @@ linters:
- misspell
- nolintlint
- sloglint
- tagalign
- unparam
- unused
- whitespace
@@ -116,6 +117,7 @@ linters:
- gosec
- gosmopolitan
- grouper
- iface
- inamedparam
- interfacebloat
- intrange
@@ -140,6 +142,7 @@ linters:
- promlinter
- protogetter
- reassign
- recvcheck
- rowserrcheck
- sloglint
- spancheck
@@ -153,7 +156,6 @@ linters:
- tparallel
- typecheck
- unconvert
- unparam
- usestdlibvars
- varnamelen
- wastedassign
+35 -11
View File
@@ -16,21 +16,21 @@ linters-settings:
desc: use `context` package instead
- pkg: golang.org/x/exp/slices
desc: use `slices` package instead
- pkg: maps
desc: use `golang.org/x/exp/maps` package instead
# - pkg: golang.org/x/exp/maps
# desc: use `maps` package instead
commonerrors:
files:
- $all
deny:
- pkg: github.com/FerretDB/FerretDB/internal/handler/commonerrors
- pkg: github.com/FerretDB/FerretDB/v2/internal/handler/commonerrors
exhaustive:
default-signifies-exhaustive: false
gci:
sections:
- standard
- default
- prefix(github.com/FerretDB/FerretDB)
- prefix(github.com/FerretDB/FerretDB/integration)
- prefix(github.com/FerretDB/FerretDB/v2)
- prefix(github.com/FerretDB/FerretDB/v2/integration)
goconst:
min-occurrences: 5
ignore-tests: true
@@ -44,6 +44,8 @@ linters-settings:
- BUG
- FIXME
- HACK
- lala
- lalala
goheader:
# Please do not update it, including bumping a year.
template: |-
@@ -61,10 +63,12 @@ linters-settings:
See the License for the specific language governing permissions and
limitations under the License.
goimports:
local-prefixes: github.com/FerretDB/FerretDB
local-prefixes: github.com/FerretDB/FerretDB/v2
gomodguard:
blocked:
modules: []
modules:
- github.com/FerretDB/FerretDB:
reason: "Use `github.com/FerretDB/FerretDB/v2/...` instead."
gosimple:
checks: ["all"]
govet:
@@ -74,13 +78,16 @@ linters-settings:
- nilness
- shadow
importas:
no-unaliased: false
no-extra-aliases: true
# https://github.com/julz/importas/issues/15
no-unaliased: true
no-extra-aliases: false
alias:
- pkg: github.com/prometheus/client_model/go
alias: dto
- pkg: github.com/jaswdr/faker
alias: fakerlib
- pkg: github.com/xdg-go/scram
alias: xdgscram
- pkg: go.opentelemetry.io/otel/attribute
alias: otelattribute
- pkg: go.opentelemetry.io/otel/codes
@@ -120,7 +127,7 @@ linters-settings:
attr-only: true
no-global: "all"
context: "scope"
static-msg: true
static-msg: false # TODO https://github.com/FerretDB/FerretDB/issues/3421
no-raw-keys: false # TODO https://github.com/FerretDB/FerretDB/issues/3421
key-naming-case: snake
args-on-sep-lines: false
@@ -128,7 +135,23 @@ linters-settings:
checks:
- all
- -SA1019 # ignore deprecation errors in existing code; new code is checked by the other configuration
# unused
tagalign:
sort: true
order:
- name
- cmd
- arg
- default
- help
unparam:
check-exported: false
unused:
field-writes-are-uses: false
post-statements-are-reads: false
exported-fields-are-used: false
parameters-are-used: true # it should be false, TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/823
local-variables-are-used: false
generated-is-used: false
whitespace:
multi-if: false
multi-func: false
@@ -157,6 +180,7 @@ linters:
- revive
- sloglint
- staticcheck
- tagalign
- unused
- whitespace
+7 -76
View File
@@ -16,13 +16,13 @@ tasks:
- >
go test -count=1 {{.RACE_FLAG}} -run=TestEnvData
-tags=ferretdb_testenvdata .
-target-backend=ferretdb-postgresql
-target-url='mongodb://127.0.0.1:27017/'
-target-backend=mongodb
-target-url='mongodb://username:password@127.0.0.1:47017/'
- >
go test -count=1 {{.RACE_FLAG}} -run=TestEnvData
-tags=ferretdb_testenvdata .
-target-backend=mongodb
-target-url='mongodb://127.0.0.1:47017/'
-target-backend=ferretdb
-target-url='mongodb://username:password@127.0.0.1:27017/'
integration-gen:
cmds:
@@ -34,7 +34,6 @@ tasks:
- ../bin/golangci-lint{{exeExt}} run --config=.golangci.yml
- ../bin/golangci-lint{{exeExt}} run --config=.golangci-new.yml
- ../bin/go-consistent{{exeExt}} -pedantic ./...
- go vet -vettool=../bin/checkswitch{{exeExt}} ./...
- go vet -vettool=../bin/checkcomments{{exeExt}} ./...
@@ -50,79 +49,11 @@ tasks:
-count={{.BENCH_COUNT}} -bench={{.BENCH_NAME}} -benchtime={{.BENCH_TIME}} -benchmem
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=ferretdb-postgresql
-postgresql-url=postgres://username@127.0.0.1:5432/ferretdb
-target-backend=ferretdb
-postgresql-url=postgres://username:password@127.0.0.1:5432/postgres
| tee new-postgresql.txt
- ../bin/benchstat{{exeExt}} old-postgresql.txt new-postgresql.txt
bench-postgresql-no-pushdown:
desc: "Run benchmarks for `postgresql` backend without filter pushdown"
cmds:
- >
go test -tags={{.BUILD_TAGS}} -timeout=0 -run=XXX
-count={{.BENCH_COUNT}} -bench={{.BENCH_NAME}} -benchtime={{.BENCH_TIME}} -benchmem
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=ferretdb-postgresql
-postgresql-url=postgres://username@127.0.0.1:5432/ferretdb
-disable-pushdown
| tee new-postgresql.txt
- ../bin/benchstat{{exeExt}} old-postgresql.txt new-postgresql.txt
bench-sqlite:
desc: "Run benchmarks for `sqlite` backend"
cmds:
- >
go test -tags={{.BUILD_TAGS}} -timeout=0 -run=XXX
-count={{.BENCH_COUNT}} -bench={{.BENCH_NAME}} -benchtime={{.BENCH_TIME}} -benchmem
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=ferretdb-sqlite
-sqlite-url=file:../tmp/sqlite-tests/
| tee new-sqlite.txt
- ../bin/benchstat{{exeExt}} old-sqlite.txt new-sqlite.txt
bench-sqlite-no-pushdown:
desc: "Run benchmarks for `sqlite` with filter pushdown disabled"
cmds:
- >
go test -tags={{.BUILD_TAGS}} -timeout=0 -run=XXX
-count={{.BENCH_COUNT}} -bench={{.BENCH_NAME}} -benchtime={{.BENCH_TIME}} -benchmem
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=ferretdb-sqlite
-sqlite-url=file:../tmp/sqlite-tests/
-disable-pushdown
| tee new-sqlite.txt
- ../bin/benchstat{{exeExt}} old-sqlite.txt new-sqlite.txt
bench-mysql:
desc: "Run benchmarks for `mysql` backend"
cmds:
- >
go test -tags={{.BUILD_TAGS}} -timeout=0 -run=XXX
-count={{.BENCH_COUNT}} -bench-{{.BENCH_NAME}} -benchtime={{.BENCH_TIME}} -benchmem
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=ferretdb-mysql
-mysql-url=mysql://username:password@127.0.0.1:3306/ferretdb
| tee new-mysql.txt
- ../bin/benchstat{{exeExt}} old-mysql.txt new-mysql.txt
bench-mysql-no-pushdown:
desc: "Run benchmarks for `mysql` with filter pushdown disabled"
cmds:
- >
go test -tags={{.BUILD_TAGS}} -timeout=0 -run=XXX
-count={{.BENCH_COUNT}} -bench={{.BENCH_NAME}} -benchtime={{.BENCH_TIME}} -benchmem
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=ferretdb-mysql
-mysql-url=mysql://username:password@127.0.0.1:3306/ferretdb
-disable-pushdown
| tee new-sqlite.txt
- ../bin/benchstat{{exeExt}} old-sqlite.txt new-sqlite.txt
bench-mongodb:
desc: "Run benchmarks for MongoDB"
cmds:
@@ -132,6 +63,6 @@ tasks:
-log-level=error
-bench-docs={{.BENCH_DOCS}}
-target-backend=mongodb
-target-url='mongodb://127.0.0.1:47017/'
-target-url='mongodb://username:password@127.0.0.1:47017/'
| tee new-mongodb.txt
- ../bin/benchstat{{exeExt}} old-mongodb.txt new-mongodb.txt
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
// Package admin contains tests for administration commands:
// - collMod;
// - compact;
// - create;
// - createIndexes;
// - currentOp;
// - drop;
// - dropDatabase;
// - dropIndexes;
// - getParameter;
// - killOp;
// - listCollections;
// - listDatabases;
// - listIndexes;
// - renameCollection;
// - setParameter.
package admin
import (
"testing"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestMain(m *testing.M) {
setup.Main(m)
}
+524
View File
@@ -0,0 +1,524 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package integration
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/v2/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
func TestAggregateCommandCollStats(tt *testing.T) {
tt.Parallel()
ctx, collection := setup.Setup(tt, shareddata.ArrayDocuments)
for name, tc := range map[string]struct { //nolint:vet // used for testing only
pipeline bson.A // required
expected bson.D // optional, defaults to the standard response with storageStats
expectedSizeIsZero bool // optional, if true, the size field is expected to be 0 (due to scale factor)
failsForFerretDB string
}{
"EmptyCollStats": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{}}}},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/534",
expected: bson.D{
{
"ns", collection.Database().Name() + "." + collection.Name(),
},
{"host", ""},
{"localTime", primitive.DateTime(0)},
},
},
"Count": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"count", bson.D{}}}}}},
expected: bson.D{
{
"ns", collection.Database().Name() + "." + collection.Name(),
},
{"host", ""},
{"localTime", primitive.DateTime(0)},
{"count", int32(4)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/534",
},
"StorageStats": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}},
expected: bson.D{
{"ns", collection.Database().Name() + "." + collection.Name()},
{"host", ""},
{"localTime", primitive.DateTime(0)},
{"storageStats", bson.D{
{"size", int32(0)},
{"count", int32(0)},
{"avgObjSize", int32(0)},
{"numOrphanDocs", int32(0)},
{"storageSize", int32(0)},
{"freeStorageSize", int32(0)},
{"capped", false},
{"nindexes", int32(0)},
{"indexDetails", bson.D{}},
{"indexBuilds", bson.A{}},
{"totalIndexSize", int32(0)},
{"indexSizes", bson.D{}},
{"totalSize", int32(0)},
{"scaleFactor", int32(1)},
}},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/534",
},
"StorageStatsWithScale": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", 1000}}}}}}},
expected: bson.D{
{"ns", collection.Database().Name() + "." + collection.Name()},
{"host", ""},
{"localTime", primitive.DateTime(0)},
{"storageStats", bson.D{
{"size", int32(0)},
{"count", int32(0)},
{"avgObjSize", int32(0)},
{"numOrphanDocs", int32(0)},
{"storageSize", int32(0)},
{"freeStorageSize", int32(0)},
{"capped", false},
{"nindexes", int32(0)},
{"indexDetails", bson.D{}},
{"indexBuilds", bson.A{}},
{"totalIndexSize", int32(0)},
{"indexSizes", bson.D{}},
{"totalSize", int32(0)},
{"scaleFactor", int32(1000)},
}},
},
expectedSizeIsZero: true,
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/534",
},
"StorageStatsFloatScale": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", 42.42}}}}}}},
expected: bson.D{
{"ns", collection.Database().Name() + "." + collection.Name()},
{"host", ""},
{"localTime", primitive.DateTime(0)},
{"storageStats", bson.D{
{"size", int32(0)},
{"count", int32(0)},
{"avgObjSize", int32(0)},
{"numOrphanDocs", int32(0)},
{"storageSize", int32(0)},
{"freeStorageSize", int32(0)},
{"capped", false},
{"nindexes", int32(0)},
{"indexDetails", bson.D{}},
{"indexBuilds", bson.A{}},
{"totalIndexSize", int32(0)},
{"indexSizes", bson.D{}},
{"totalSize", int32(0)},
{"scaleFactor", int32(42)},
}},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/534",
},
"CountAndStorageStats": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}}},
expected: bson.D{
{"ns", collection.Database().Name() + "." + collection.Name()},
{"host", ""},
{"localTime", primitive.DateTime(0)},
{"storageStats", bson.D{
{"size", int32(0)},
{"count", int32(0)},
{"avgObjSize", int32(0)},
{"numOrphanDocs", int32(0)},
{"storageSize", int32(0)},
{"freeStorageSize", int32(0)},
{"capped", false},
{"nindexes", int32(0)},
{"indexDetails", bson.D{}},
{"indexBuilds", bson.A{}},
{"totalIndexSize", int32(0)},
{"indexSizes", bson.D{}},
{"totalSize", int32(0)},
{"scaleFactor", int32(1)},
}},
{"count", int32(4)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/534",
},
"CollStatsCount": {
pipeline: bson.A{
bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}},
bson.D{{"$count", "after"}},
},
expected: bson.D{{"after", int32(1)}},
},
} {
tt.Run(name, func(tt *testing.T) {
var t testing.TB = tt
tt.Parallel()
require.NotNil(tt, tc.pipeline, "pipeline must be set")
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
// call validate to updated statistics
err := collection.Database().RunCommand(ctx, bson.D{{"validate", collection.Name()}}).Err()
require.NoError(t, err)
cursor, err := collection.Aggregate(ctx, tc.pipeline)
require.NoError(t, err)
res := FetchAll(t, ctx, cursor)
require.Len(t, res, 1)
var actualComparable bson.D
for _, field := range res[0] {
switch field.Key {
case "host":
var port string
_, port, err = net.SplitHostPort(field.Value.(string))
require.NoError(t, err)
assert.NotEmpty(t, port)
actualComparable = append(actualComparable, bson.E{Key: field.Key, Value: ""})
case "localTime":
assert.IsType(t, primitive.DateTime(0), field.Value)
actualComparable = append(actualComparable, bson.E{Key: field.Key, Value: primitive.DateTime(0)})
case "storageStats":
storageStats := field.Value.(bson.D)
require.NotEmpty(t, storageStats)
var statsComparable bson.D
for _, stat := range storageStats {
switch stat.Key {
case "freeStorageSize":
statsComparable = append(statsComparable, bson.E{Key: stat.Key, Value: int32(0)})
case "size":
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/556
size, ok := stat.Value.(int32)
require.True(t, ok)
if tc.expectedSizeIsZero {
assert.Zero(t, size)
} else {
assert.Greater(t, size, int32(0))
}
statsComparable = append(statsComparable, bson.E{Key: stat.Key, Value: int32(0)})
case "count", "avgObjSize", "storageSize", "nindexes", "totalIndexSize", "totalSize":
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/556
val, ok := stat.Value.(int32)
require.True(t, ok)
assert.Greater(t, val, int32(0), "field %s", stat.Key)
statsComparable = append(statsComparable, bson.E{Key: stat.Key, Value: int32(0)})
case "indexSizes":
assert.NotEmpty(t, stat.Value.(bson.D))
for _, indexSize := range stat.Value.(bson.D) {
assert.Greater(t, indexSize.Value, int32(0))
}
statsComparable = append(statsComparable, bson.E{Key: stat.Key, Value: bson.D{}})
case "indexDetails":
assert.NotEmpty(t, stat.Value.(bson.D))
statsComparable = append(statsComparable, bson.E{Key: stat.Key, Value: bson.D{}})
case "indexBuilds":
assert.IsType(t, bson.A{}, stat.Value)
statsComparable = append(statsComparable, bson.E{Key: stat.Key, Value: bson.A{}})
case "wiredTiger":
// exclusive to MongoDB
default:
statsComparable = append(statsComparable, stat)
}
}
actualComparable = append(actualComparable, bson.E{Key: field.Key, Value: statsComparable})
default:
actualComparable = append(actualComparable, field)
}
}
AssertEqualDocuments(t, tc.expected, actualComparable)
})
}
}
func TestAggregateCommandCollStatsErrors(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
for name, tc := range map[string]struct { //nolint:vet // used for test only
command bson.D // required, command to run
database *mongo.Database // defaults to collection.Database()
err *mongo.CommandError // required
altMessage string // optional, alternative error message
failsForFerretDB string
}{
"NonExistentDatabase": {
database: collection.Database().Client().Database("non-existent"),
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 26,
Name: "NamespaceNotFound",
Message: `PlanExecutor error during aggregation :: caused by :: ` +
`Unable to retrieve storageStats in $collStats stage :: caused by :: ` +
`Collection [non-existent.TestAggregateCommandCollStatsErrors] not found.`,
},
altMessage: "Collection [non-existent.TestAggregateCommandCollStatsErrors] not found.",
},
"NonExistentCollection": {
command: bson.D{
{"aggregate", "non-existent"},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 26,
Name: "NamespaceNotFound",
Message: `PlanExecutor error during aggregation :: caused by :: ` +
`Unable to retrieve storageStats in $collStats stage :: caused by :: ` +
`Collection [TestAggregateCommandCollStatsErrors.non-existent] not found.`,
},
altMessage: "Collection [TestAggregateCommandCollStatsErrors.non-existent] not found.",
},
"NilCollStats": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", nil}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 5447000,
Name: "Location5447000",
Message: `$collStats must take a nested object but found: $collStats: null`,
},
altMessage: `$collStats must take a nested object but found: { $collStats: null }`,
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/349",
},
"StorageStatsNegativeScale": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", -1000}}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 51024,
Name: "Location51024",
Message: `BSON field 'scale' value must be >= 1, actual value '-1000'`,
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/536",
},
"StorageStatsInvalidScale": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", "invalid"}}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: `BSON field '$collStats.storageStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double']`,
},
altMessage: `BSON field '$collStats.storageStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double]'`,
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/536",
},
"CountCollStatsCount": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{
bson.D{{"$count", "before"}},
bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}},
bson.D{{"$count", "after"}},
}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 40602,
Name: "Location40602",
Message: `$collStats is only valid as the first stage in a pipeline`,
},
altMessage: `$collStats is only valid as the first stage in the pipeline.`,
},
} {
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.err, "err must not be nil")
db := tc.database
if db == nil {
db = collection.Database()
}
var res bson.D
err := db.RunCommand(ctx, tc.command).Decode(&res)
AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
require.Nil(t, res)
})
}
}
func TestAggregateCommandCollStatsIndexSizes(tt *testing.T) {
tt.Parallel()
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/538")
ctx, collection := setup.Setup(tt, shareddata.DocumentsStrings)
cursorNoScale, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}},
})
require.NoError(t, err)
defer cursorNoScale.Close(ctx)
scale := int32(1000)
cursor, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", scale}}}}}},
})
require.NoError(t, err)
defer cursor.Close(ctx)
resNoScale := FetchAll(t, ctx, cursorNoScale)
require.Equal(t, 1, len(resNoScale))
res := FetchAll(t, ctx, cursor)
require.Equal(t, 1, len(res))
var resComparable bson.D
for _, field := range res[0] {
switch field.Key {
case "localTime":
assert.IsType(t, primitive.DateTime(0), field.Value)
resComparable = append(resComparable, bson.E{Key: field.Key, Value: primitive.DateTime(0)})
case "storageStats":
storageStats := field.Value.(bson.D)
require.NotNil(t, storageStats)
var storageStatsComparable bson.D
for _, stat := range storageStats {
switch stat.Key {
case "scaleFactor":
assert.Equal(t, scale, stat.Value)
storageStatsComparable = append(storageStatsComparable, bson.E{Key: stat.Key, Value: int32(1)})
default:
storageStatsComparable = append(storageStatsComparable, stat)
}
}
resComparable = append(resComparable, bson.E{Key: field.Key, Value: storageStatsComparable})
default:
resComparable = append(resComparable, field)
}
}
var resNoScaleComparable bson.D
for _, field := range resNoScale[0] {
switch field.Key {
case "localTime":
assert.IsType(t, primitive.DateTime(0), field.Value)
resNoScaleComparable = append(resNoScaleComparable, bson.E{Key: field.Key, Value: primitive.DateTime(0)})
case "storageStats":
storageStats := field.Value.(bson.D)
require.NotNil(t, storageStats)
var storageStatsComparable bson.D
for _, stat := range storageStats {
switch stat.Key {
case "size", "storageSize", "freeStorageSize", "totalIndexSize", "totalSize":
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/556
sizeNoScale, ok := stat.Value.(int32)
require.True(t, ok)
storageStatsComparable = append(storageStatsComparable, bson.E{Key: stat.Key, Value: sizeNoScale / scale})
case "indexSizes":
var indexSizesComparable bson.D
for _, indexSizeNoScale := range stat.Value.(bson.D) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/556
sizeNoScale, ok := indexSizeNoScale.Value.(int32)
require.True(t, ok)
indexSizesComparable = append(indexSizesComparable, bson.E{Key: indexSizeNoScale.Key, Value: sizeNoScale / scale})
}
storageStatsComparable = append(storageStatsComparable, bson.E{Key: stat.Key, Value: indexSizesComparable})
case "scaleFactor":
assert.Equal(t, int32(1), stat.Value)
storageStatsComparable = append(storageStatsComparable, stat)
default:
storageStatsComparable = append(storageStatsComparable, stat)
}
}
resNoScaleComparable = append(resNoScaleComparable, bson.E{Key: field.Key, Value: storageStatsComparable})
default:
resNoScaleComparable = append(resNoScaleComparable, field)
}
}
AssertEqualDocuments(t, resComparable, resNoScaleComparable)
}
@@ -18,6 +18,8 @@ import (
"testing"
"go.mongodb.org/mongo-driver/bson"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
func TestAggregateCompatMatchExpr(t *testing.T) {
@@ -28,11 +30,15 @@ func TestAggregateCompatMatchExpr(t *testing.T) {
pipeline: bson.A{
bson.D{{"$match", bson.D{{"$expr", "$v"}}}},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/362",
failsProviders: []shareddata.Provider{shareddata.Decimal128s, shareddata.Doubles, shareddata.Int64s, shareddata.Scalars},
},
"Sum": {
pipeline: bson.A{bson.D{{"$match", bson.D{
{"$expr", bson.D{{"$sum", "$v"}}},
}}}},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/362",
failsProviders: []shareddata.Provider{shareddata.Decimal128s, shareddata.Doubles, shareddata.Int64s, shareddata.Scalars},
},
"Gt": {
pipeline: bson.A{bson.D{{"$match", bson.D{
-181
View File
@@ -1,181 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package integration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
)
func TestAggregateCompatCollStats(t *testing.T) {
t.Parallel()
s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{shareddata.ArrayDocuments},
AddNonExistentCollection: true,
})
ctx, targetCollections, compatCollections := s.Ctx, s.TargetCollections, s.CompatCollections
for name, tc := range map[string]struct {
pipeline bson.A // required
resultType compatTestCaseResultType // defaults to nonEmptyResult
skip string // always skip this test case, must have issue number mentioned
}{
"EmptyCollStats": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{}}}},
},
"Count": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"count", bson.D{}}}}}},
},
"StorageStats": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}},
},
"StorageStatsWithScale": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", 1000}}}}}}},
},
"StorageStatsFloatScale": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", 42.42}}}}}}},
},
"CountAndStorageStats": {
pipeline: bson.A{bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}}},
},
"CollStatsCount": {
pipeline: bson.A{
bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}},
bson.D{{"$count", "after"}},
},
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
require.NotNil(t, tc.pipeline, "pipeline must be set")
var nonEmptyResults bool
for i := range targetCollections {
targetCollection := targetCollections[i]
compatCollection := compatCollections[i]
t.Run(targetCollection.Name(), func(t *testing.T) {
require.Equal(t, compatCollection.Name(), targetCollection.Name())
// call validate to updated statistics
command := bson.D{{"validate", targetCollection.Name()}}
targetErr := targetCollection.Database().RunCommand(ctx, command).Err()
compatErr := compatCollection.Database().RunCommand(ctx, command).Err()
if targetErr != nil {
t.Logf("Target error: %v", targetErr)
t.Logf("Compat error: %v", compatErr)
// error messages are intentionally not compared
AssertMatchesCommandError(t, compatErr, targetErr)
}
targetCursor, targetErr := targetCollection.Aggregate(ctx, tc.pipeline)
compatCursor, compatErr := compatCollection.Aggregate(ctx, tc.pipeline)
if targetCursor != nil {
defer targetCursor.Close(ctx)
}
if compatCursor != nil {
defer compatCursor.Close(ctx)
}
if targetErr != nil {
t.Logf("Target error: %v", targetErr)
t.Logf("Compat error: %v", compatErr)
// error messages are intentionally not compared
AssertMatchesCommandError(t, compatErr, targetErr)
return
}
require.NoError(t, compatErr, "compat error; target returned no error")
targetRes := FetchAll(t, ctx, targetCursor)
compatRes := FetchAll(t, ctx, compatCursor)
// $collStats returns one document per shard.
require.Equal(t, 1, len(compatRes))
require.Equal(t, 1, len(targetRes))
// Check the keys are the same
targetKeys := CollectKeys(t, targetRes[0])
compatKeys := CollectKeys(t, compatRes[0])
require.Equal(t, compatKeys, targetKeys)
if len(targetRes) > 0 || len(compatRes) > 0 {
nonEmptyResults = true
}
targetDoc := ConvertDocument(t, targetRes[0])
compatDoc := ConvertDocument(t, compatRes[0])
targetNs, _ := targetDoc.Get("ns")
compatNs, _ := compatDoc.Get("ns")
require.Equal(t, compatNs, targetNs)
targetCount, _ := targetDoc.Get("count")
compatCount, _ := compatDoc.Get("count")
require.EqualValues(t, compatCount, targetCount)
targetStorageStatsV, _ := targetDoc.Get("storageStats")
compatStorageStatsV, _ := compatDoc.Get("storageStats")
if targetStorageStatsV == nil {
require.Equal(t, compatStorageStatsV, targetStorageStatsV)
return
}
targetStorageStats := targetStorageStatsV.(*types.Document)
compatStorageStats := compatStorageStatsV.(*types.Document)
targetStorageCount, _ := targetStorageStats.Get("count")
compatStorageCount, _ := compatStorageStats.Get("count")
require.EqualValues(t, compatStorageCount, targetStorageCount)
targetStorageIndexes, _ := targetStorageStats.Get("nindexes")
compatStorageIndexes, _ := compatStorageStats.Get("nindexes")
require.EqualValues(t, compatStorageIndexes, targetStorageIndexes)
targetStorageScaleFactor, _ := targetStorageStats.Get("scaleFactor")
compatStorageScaleFactor, _ := compatStorageStats.Get("scaleFactor")
require.Equal(t, compatStorageScaleFactor, targetStorageScaleFactor)
})
}
switch tc.resultType {
case nonEmptyResult:
assert.True(t, nonEmptyResults, "expected non-empty results (some documents should be returned)")
case emptyResult:
assert.False(t, nonEmptyResults, "expected empty results (no documents should be returned)")
default:
t.Fatalf("unknown result type %v", tc.resultType)
}
})
}
}
-313
View File
@@ -1,313 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package integration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
)
func TestAggregateCollStats(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.DocumentsStrings)
// call validate to updated statistics
err := collection.Database().RunCommand(ctx, bson.D{{"validate", collection.Name()}}).Err()
require.NoError(t, err)
pipeline := bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}
cursor, err := collection.Aggregate(ctx, pipeline)
require.NoError(t, err)
res := FetchAll(t, ctx, cursor)
require.Len(t, res, 1)
doc := ConvertDocument(t, res[0])
assert.Equal(t, collection.Database().Name()+"."+collection.Name(), must.NotFail(doc.Get("ns")))
v, _ := doc.Get("storageStats")
require.NotNil(t, v)
storageStats, ok := v.(*types.Document)
require.True(t, ok)
assert.NotZero(t, must.NotFail(storageStats.Get("size")))
assert.NotZero(t, must.NotFail(storageStats.Get("count")))
assert.NotZero(t, must.NotFail(storageStats.Get("avgObjSize")))
assert.NotZero(t, must.NotFail(storageStats.Get("storageSize")))
assert.Zero(t, must.NotFail(storageStats.Get("freeStorageSize")))
assert.Equal(t, false, must.NotFail(storageStats.Get("capped")))
assert.NotZero(t, must.NotFail(storageStats.Get("nindexes")))
assert.NotZero(t, must.NotFail(storageStats.Get("totalIndexSize")))
assert.NotZero(t, must.NotFail(storageStats.Get("totalSize")))
assert.NotZero(t, must.NotFail(storageStats.Get("indexSizes")))
assert.Equal(t, int32(1), must.NotFail(storageStats.Get("scaleFactor")))
cappedCollectionName := testutil.CollectionName(t) + "capped"
opts := options.CreateCollection().SetCapped(true).SetSizeInBytes(1000).SetMaxDocuments(10)
err = collection.Database().CreateCollection(ctx, cappedCollectionName, opts)
require.NoError(t, err)
cappedCollection := collection.Database().Collection(cappedCollectionName)
cursor, err = cappedCollection.Aggregate(ctx, pipeline)
require.NoError(t, err)
res = FetchAll(t, ctx, cursor)
require.Len(t, res, 1)
v, _ = ConvertDocument(t, res[0]).Get("storageStats")
require.NotNil(t, v)
storageStats, ok = v.(*types.Document)
require.True(t, ok)
assert.Equal(t, true, must.NotFail(storageStats.Get("capped")))
}
func TestAggregateCollStatsCommandErrors(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
for name, tc := range map[string]struct { //nolint:vet // used for test only
command bson.D // required, command to run
database *mongo.Database // defaults to collection.Database()
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
}{
"NonExistentDatabase": {
database: collection.Database().Client().Database("non-existent"),
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 26,
Name: "NamespaceNotFound",
Message: `PlanExecutor error during aggregation :: caused by :: ` +
`Unable to retrieve storageStats in $collStats stage :: caused by :: ` +
`Collection [non-existent.TestAggregateCollStatsCommandErrors] not found.`,
},
altMessage: "ns not found: non-existent.TestAggregateCollStatsCommandErrors",
},
"NonExistentCollection": {
command: bson.D{
{"aggregate", "non-existent"},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 26,
Name: "NamespaceNotFound",
Message: `PlanExecutor error during aggregation :: caused by :: ` +
`Unable to retrieve storageStats in $collStats stage :: caused by :: ` +
`Collection [TestAggregateCollStatsCommandErrors.non-existent] not found.`,
},
altMessage: "ns not found: TestAggregateCollStatsCommandErrors.non-existent",
},
"NilCollStats": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", nil}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 5447000,
Name: "Location5447000",
Message: `$collStats must take a nested object but found: $collStats: null`,
},
altMessage: `$collStats must take a nested object but found: { $collStats: null }`,
},
"StorageStatsNegativeScale": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", -1000}}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 51024,
Name: "Location51024",
Message: `BSON field 'scale' value must be >= 1, actual value '-1000'`,
},
},
"StorageStatsInvalidScale": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", "invalid"}}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: `BSON field '$collStats.storageStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double']`,
},
altMessage: `BSON field '$collStats.storageStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double]'`,
},
"CountCollStatsCount": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{
bson.D{{"$count", "before"}},
bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}},
bson.D{{"$count", "after"}},
}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 40602,
Name: "Location40602",
Message: `$collStats is only valid as the first stage in a pipeline`,
},
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
require.NotNil(t, tc.err, "err must not be nil")
db := tc.database
if db == nil {
db = collection.Database()
}
var res bson.D
err := db.RunCommand(ctx, tc.command).Decode(&res)
AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
require.Nil(t, res)
})
}
}
func TestAggregateCollStatsCommandIndexSizes(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.DocumentsStrings)
cursorNoScale, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}},
})
require.NoError(t, err)
defer cursorNoScale.Close(ctx)
scale := int32(1000)
cursor, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", scale}}}}}},
})
require.NoError(t, err)
defer cursor.Close(ctx)
resNoScale := FetchAll(t, ctx, cursorNoScale)
require.Equal(t, 1, len(resNoScale))
res := FetchAll(t, ctx, cursor)
require.Equal(t, 1, len(res))
docNoScale := ConvertDocument(t, resNoScale[0])
doc := ConvertDocument(t, res[0])
storageStatsNoScale := must.NotFail(docNoScale.Get("storageStats")).(*types.Document)
storageStats := must.NotFail(doc.Get("storageStats")).(*types.Document)
size := must.NotFail(storageStats.Get("size"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("size")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, size)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), size)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
avgObjSizeNoScale := must.NotFail(storageStatsNoScale.Get("avgObjSize"))
avgObjSize := must.NotFail(storageStats.Get("avgObjSize"))
require.EqualValues(t, avgObjSizeNoScale, avgObjSize)
storageSize := must.NotFail(storageStats.Get("storageSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("storageSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, storageSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), storageSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
freeStorageSize := must.NotFail(storageStats.Get("freeStorageSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("freeStorageSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, freeStorageSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), freeStorageSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
totalIndexSize := must.NotFail(storageStats.Get("totalIndexSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("totalIndexSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, totalIndexSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), totalIndexSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
totalSize := must.NotFail(storageStats.Get("totalSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("totalSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, totalSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), totalSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
indexSizesNoScale := must.NotFail(storageStatsNoScale.Get("indexSizes")).(*types.Document)
indexSizes := must.NotFail(storageStats.Get("indexSizes")).(*types.Document)
require.Equal(t, []string{"_id_"}, indexSizesNoScale.Keys())
require.Equal(t, []string{"_id_"}, indexSizes.Keys())
for _, index := range indexSizesNoScale.Keys() {
size := must.NotFail(indexSizes.Get(index))
switch sizeNoScale := must.NotFail(indexSizesNoScale.Get(index)).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, size)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), size)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
}
}
@@ -24,8 +24,10 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
"github.com/FerretDB/FerretDB/v2/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
func TestAggregateAddFieldsErrors(t *testing.T) {
@@ -36,9 +38,9 @@ func TestAggregateAddFieldsErrors(t *testing.T) {
for name, tc := range map[string]struct { //nolint:vet // used for test only
pipeline bson.A // required, aggregation pipeline stages
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required
altMessage string // optional, alternative error message
failsForFerretDB string
}{
"NotDocument": {
pipeline: bson.A{
@@ -49,7 +51,7 @@ func TestAggregateAddFieldsErrors(t *testing.T) {
Name: "Location40272",
Message: "$addFields specification stage must be an object, got string",
},
altMessage: "$addFields specification stage must be an object, got string",
altMessage: "$addFields specification stage must be an object",
},
"InvalidFieldPath": {
pipeline: bson.A{
@@ -60,11 +62,16 @@ func TestAggregateAddFieldsErrors(t *testing.T) {
Name: "Location16410",
Message: "Invalid $addFields :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
@@ -82,6 +89,35 @@ func TestAggregateAddFieldsErrors(t *testing.T) {
}
}
func TestAggregateGroupSumDecimalDouble(tt *testing.T) {
tt.Parallel()
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/1056")
ctx, collection := setup.Setup(t)
_, err := collection.InsertMany(ctx, bson.A{
bson.D{{"_id", "decimal128"}, {"v", must.NotFail(primitive.ParseDecimal128("42.1"))}},
bson.D{{"_id", "double"}, {"v", float64(42.1)}},
})
require.NoError(t, err)
cursor, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$group", bson.D{{"_id", nil}, {"sum", bson.D{{"$sum", "$v"}}}}}},
})
require.NoError(t, err)
var res bson.A
require.NoError(t, cursor.All(ctx, &res))
expected := bson.A{bson.D{
{"_id", nil},
{"sum", primitive.NewDecimal128(3459220962935157325, 6906845732440572485)}, // 84.20000000000000142108547152020037
}}
assert.Equal(t, expected, res)
}
func TestAggregateGroupErrors(t *testing.T) {
t.Parallel()
@@ -90,9 +126,10 @@ func TestAggregateGroupErrors(t *testing.T) {
for name, tc := range map[string]struct {
pipeline bson.A // required, aggregation pipeline stages
err *mongo.CommandError // required, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
skip string // TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/1086
failsForFerretDB string
}{
"UnaryOperatorSum": {
pipeline: bson.A{
@@ -103,7 +140,8 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location40237",
Message: "The $sum accumulator is a unary operator",
},
altMessage: "The $sum accumulator is a unary operator",
altMessage: "The $sum accumulator is a unary operator",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/389",
},
"TypeEmpty": {
pipeline: bson.A{
@@ -114,6 +152,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location40234",
Message: "The field 'v' must be an accumulator object",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/389",
},
"TwoOperators": {
pipeline: bson.A{
@@ -124,7 +163,8 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location15983",
Message: "An object representing an expression must have exactly one field: { $type: 42, $op: 42 }",
},
altMessage: "An object representing an expression must have exactly one field",
altMessage: "An object representing an expression must have exactly one field",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"TypeInvalidLen": {
pipeline: bson.A{
@@ -135,6 +175,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location16020",
Message: "Expression $type takes exactly 1 arguments. 2 were passed in.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"NonExistentOperator": {
pipeline: bson.A{
@@ -145,6 +186,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "InvalidPipelineOperator",
Message: "Unrecognized expression '$non-existent'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"SumEmptyExpression": {
pipeline: bson.A{
@@ -157,6 +199,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location16872",
Message: "'$' by itself is not a valid FieldPath",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"SumEmptyVariable": {
pipeline: bson.A{
@@ -169,6 +212,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "FailedToParse",
Message: "empty variable names are not allowed",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"SumDollarVariable": {
pipeline: bson.A{
@@ -181,6 +225,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "FailedToParse",
Message: "'$' starts with an invalid character for a user variable name",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"RecursiveNonExistentOperator": {
pipeline: bson.A{
@@ -191,6 +236,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "InvalidPipelineOperator",
Message: "Unrecognized expression '$non-existent'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"IDExpressionDuplicateFields": {
pipeline: bson.A{
@@ -206,6 +252,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location16406",
Message: "duplicate field name specified in object literal: { v: \"$v\", v: \"$non-existent\" }",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"IDExpressionEmptyPath": {
pipeline: bson.A{
@@ -218,6 +265,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "Location16872",
Message: "'$' by itself is not a valid FieldPath",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"IDExpressionEmptyVariable": {
pipeline: bson.A{
@@ -230,6 +278,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "FailedToParse",
Message: "empty variable names are not allowed",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"IDExpressionInvalidVariable$": {
pipeline: bson.A{
@@ -242,6 +291,7 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "FailedToParse",
Message: "'$' starts with an invalid character for a user variable name",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"IDExpressionInvalidVariable$s": {
pipeline: bson.A{
@@ -254,7 +304,8 @@ func TestAggregateGroupErrors(t *testing.T) {
Name: "FailedToParse",
Message: "'$s' starts with an invalid character for a user variable name",
},
altMessage: "'$' starts with an invalid character for a user variable name",
altMessage: "'$' starts with an invalid character for a user variable name",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/390",
},
"IDExpressionNonExistingVariable": {
pipeline: bson.A{
@@ -270,9 +321,17 @@ func TestAggregateGroupErrors(t *testing.T) {
skip: "https://github.com/FerretDB/FerretDB/issues/2275",
},
} {
t.Run(name, func(t *testing.T) {
t.Run(name, func(tt *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
tt.Skip(tc.skip)
}
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
@@ -294,9 +353,10 @@ func TestAggregateProjectErrors(t *testing.T) {
for name, tc := range map[string]struct {
pipeline bson.A // required, aggregation pipeline stages
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/1086
failsForFerretDB string
}{
"EmptyPipeline": {
pipeline: bson.A{
@@ -307,6 +367,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location51272",
Message: "Invalid $project :: caused by :: projection specification must have at least one field",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"EmptyProjectionField": {
pipeline: bson.A{
@@ -329,6 +390,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"FieldPath cannot be constructed with empty string",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"EmptyPath": {
pipeline: bson.A{
@@ -339,6 +401,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location15998",
Message: "Invalid $project :: caused by :: FieldPath field names may not be empty strings.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ExcludeInclude": {
pipeline: bson.A{
@@ -349,7 +412,8 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location31253",
Message: "Invalid $project :: caused by :: Cannot do inclusion on field bar in exclusion projection",
},
altMessage: "Cannot do inclusion on field bar in exclusion projection",
altMessage: "Cannot do inclusion on field bar in exclusion projection",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"IncludeExclude": {
pipeline: bson.A{
@@ -360,7 +424,8 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location31254",
Message: "Invalid $project :: caused by :: Cannot do exclusion on field bar in inclusion projection",
},
altMessage: "Cannot do exclusion on field bar in inclusion projection",
altMessage: "Cannot do exclusion on field bar in inclusion projection",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorMultiple": {
pipeline: bson.A{
@@ -372,6 +437,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"Cannot use positional projection in aggregation projection",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorMiddle": {
pipeline: bson.A{
@@ -392,6 +458,7 @@ func TestAggregateProjectErrors(t *testing.T) {
"for example: a.b.$. If the query previously used a form " +
"like a.b.$.d, remove the parts following the '$' and " +
"the results will be equivalent.",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorWrongLocations": {
pipeline: bson.A{
@@ -412,6 +479,7 @@ func TestAggregateProjectErrors(t *testing.T) {
"for example: a.b.$. If the query previously used a form " +
"like a.b.$.d, remove the parts following the '$' and " +
"the results will be equivalent.",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorEmptyPath": {
pipeline: bson.A{
@@ -423,6 +491,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"Cannot use positional projection in aggregation projection",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorDollarKey": {
pipeline: bson.A{
@@ -434,6 +503,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorDollarInKey": {
pipeline: bson.A{
@@ -445,6 +515,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorDollarPrefix": {
pipeline: bson.A{
@@ -456,6 +527,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorDotDollarInKey": {
pipeline: bson.A{
@@ -467,6 +539,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorPrefixSuffix": {
pipeline: bson.A{
@@ -478,6 +551,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"Cannot use positional projection in aggregation projection",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"PositionalOperatorExclusion": {
pipeline: bson.A{
@@ -489,6 +563,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: " +
"Cannot use positional projection in aggregation projection",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ProjectPositionalOperator": {
pipeline: bson.A{
@@ -499,6 +574,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location31324",
Message: "Invalid $project :: caused by :: Cannot use positional projection in aggregation projection",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ProjectTypeEmpty": {
pipeline: bson.A{
@@ -509,6 +585,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location51270",
Message: "Invalid $project :: caused by :: An empty sub-projection is not a valid value." + " Found empty object at path",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ProjectTwoOperators": {
pipeline: bson.A{
@@ -519,6 +596,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location16410",
Message: "Invalid $project :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ProjectTypeInvalidLen": {
pipeline: bson.A{
@@ -529,6 +607,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location16020",
Message: "Invalid $project :: caused by :: Expression $type takes exactly 1 arguments. 2 were passed in.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ProjectNonExistentOperator": {
pipeline: bson.A{
@@ -540,6 +619,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location31325",
Message: "Invalid $project :: caused by :: Unknown expression $non-existent",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"ProjectRecursiveNonExistentOperator": {
pipeline: bson.A{
@@ -550,6 +630,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "InvalidPipelineOperator",
Message: "Invalid $project :: caused by :: Unrecognized expression '$non-existent'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"SumEmptyExpression": {
pipeline: bson.A{
@@ -562,6 +643,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "Location16872",
Message: "Invalid $project :: caused by :: '$' by itself is not a valid FieldPath",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"SumEmptyVariable": {
pipeline: bson.A{
@@ -574,6 +656,7 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "FailedToParse",
Message: "Invalid $project :: caused by :: empty variable names are not allowed",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
"SumDollarVariable": {
pipeline: bson.A{
@@ -586,11 +669,20 @@ func TestAggregateProjectErrors(t *testing.T) {
Name: "FailedToParse",
Message: "Invalid $project :: caused by :: '$' starts with an invalid character for a user variable name",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/369",
},
} {
t.Run(name, func(t *testing.T) {
t.Run(name, func(tt *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
tt.Skip(tc.skip)
}
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
@@ -610,8 +702,7 @@ func TestAggregateProject(t *testing.T) {
for name, tc := range map[string]struct { //nolint:vet // used for testing only
pipeline bson.A // required, aggregation pipeline stages
res []bson.D // required, expected response
skip string // optional, skip test with a specified reason
res []bson.D // required, expected response
}{
"IDFalseValueTrue": {
pipeline: bson.A{
@@ -629,9 +720,7 @@ func TestAggregateProject(t *testing.T) {
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
t.Parallel()
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
require.NotNil(t, tc.res, "res must not be nil")
@@ -656,9 +745,9 @@ func TestAggregateSetErrors(t *testing.T) {
for name, tc := range map[string]struct { //nolint:vet // used for test only
pipeline bson.A // required, aggregation pipeline stages
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required
altMessage string // optional, alternative error message
failsForFerretDB string
}{
"NotDocument": {
pipeline: bson.A{
@@ -669,7 +758,7 @@ func TestAggregateSetErrors(t *testing.T) {
Name: "Location40272",
Message: "$set specification stage must be an object, got string",
},
altMessage: "$set specification stage must be an object, got string",
altMessage: "$addFields specification stage must be an object",
},
"InvalidFieldPath": {
pipeline: bson.A{
@@ -680,11 +769,16 @@ func TestAggregateSetErrors(t *testing.T) {
Name: "Location16410",
Message: "Invalid $set :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
@@ -704,9 +798,9 @@ func TestAggregateUnsetErrors(t *testing.T) {
for name, tc := range map[string]struct { //nolint:vet // used for test only
pipeline bson.A // required, aggregation pipeline stages
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required
altMessage string // optional, alternative error message
failsForFerretDB string
}{
"EmptyString": {
pipeline: bson.A{
@@ -717,6 +811,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location40352",
Message: "Invalid $unset :: caused by :: FieldPath cannot be constructed with empty string",
},
altMessage: "FieldPath cannot be constructed with empty string",
},
"InvalidType": {
pipeline: bson.A{
@@ -737,6 +832,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location15998",
Message: "Invalid $unset :: caused by :: FieldPath field names may not be empty strings.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"PathEmptySuffixKey": {
pipeline: bson.A{
@@ -747,6 +843,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location40353",
Message: "Invalid $unset :: caused by :: FieldPath must not end with a '.'.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"PathEmptyPrefixKey": {
pipeline: bson.A{
@@ -757,6 +854,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location15998",
Message: "Invalid $unset :: caused by :: FieldPath field names may not be empty strings.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"PathDollarPrefix": {
pipeline: bson.A{
@@ -767,6 +865,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location16410",
Message: "Invalid $unset :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
altMessage: "FieldPath field names may not start with '$'",
},
"ArrayEmpty": {
pipeline: bson.A{
@@ -798,6 +897,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location40352",
Message: "Invalid $unset :: caused by :: FieldPath cannot be constructed with empty string",
},
altMessage: "FieldPath cannot be constructed with empty string",
},
"ArrayPathDuplicate": {
pipeline: bson.A{
@@ -808,6 +908,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location31250",
Message: "Invalid $unset :: caused by :: Path collision at v",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathOverwrites": {
pipeline: bson.A{
@@ -818,6 +919,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location31249",
Message: "Invalid $unset :: caused by :: Path collision at v.foo remaining portion foo",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathOverwritesRemaining": {
pipeline: bson.A{
@@ -828,6 +930,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location31249",
Message: "Invalid $unset :: caused by :: Path collision at v.foo.bar remaining portion foo.bar",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathCollision": {
pipeline: bson.A{
@@ -838,6 +941,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location31250",
Message: "Invalid $unset :: caused by :: Path collision at v",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathEmptyKey": {
pipeline: bson.A{
@@ -848,6 +952,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location15998",
Message: "Invalid $unset :: caused by :: FieldPath field names may not be empty strings.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathEmptySuffixKey": {
pipeline: bson.A{
@@ -858,6 +963,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location40353",
Message: "Invalid $unset :: caused by :: FieldPath must not end with a '.'.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathEmptyPrefixKey": {
pipeline: bson.A{
@@ -868,6 +974,7 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location15998",
Message: "Invalid $unset :: caused by :: FieldPath field names may not be empty strings.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"ArrayPathDollarPrefix": {
pipeline: bson.A{
@@ -878,11 +985,16 @@ func TestAggregateUnsetErrors(t *testing.T) {
Name: "Location16410",
Message: "Invalid $unset :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField.",
},
altMessage: "FieldPath field names may not start with '$'",
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
@@ -902,9 +1014,9 @@ func TestAggregateSortErrors(t *testing.T) {
for name, tc := range map[string]struct { //nolint:vet // used for test only
pipeline bson.A // required, aggregation pipeline stages
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required
altMessage string // optional, alternative error message
failsForFerretDB string
}{
"DotNotationMissingField": {
pipeline: bson.A{bson.D{{"$sort", bson.D{
@@ -915,11 +1027,16 @@ func TestAggregateSortErrors(t *testing.T) {
Name: "Location15998",
Message: "FieldPath field names may not be empty strings.",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/349",
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.pipeline, "pipeline must not be nil")
@@ -933,15 +1050,14 @@ func TestAggregateSortErrors(t *testing.T) {
func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
for name, tc := range map[string]struct { //nolint:vet // used for testing only
command bson.D // required, command to run
err *mongo.CommandError // required, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
skip string // optional, skip test with a specified reason
err *mongo.CommandError // required, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
failsForFerretDB string
}{
"NegativeLong": {
command: bson.D{
@@ -955,6 +1071,7 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "Location51024",
Message: "BSON field 'maxTimeMS' value must be >= 0, actual value '-1'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"MaxLong": {
command: bson.D{
@@ -968,7 +1085,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "BadValue",
Message: "9223372036854775807 value for maxTimeMS is out of range " + shareddata.Int32Interval,
},
altMessage: "9223372036854775807 value for maxTimeMS is out of range",
altMessage: "9223372036854775807 value for maxTimeMS is out of range",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"Double": {
command: bson.D{
@@ -982,6 +1100,7 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "BadValue",
Message: "maxTimeMS has non-integral value",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"NegativeDouble": {
command: bson.D{
@@ -995,7 +1114,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "Location51024",
Message: "BSON field 'maxTimeMS' value must be >= 0, actual value '-14245345234123246'",
},
altMessage: "BSON field 'maxTimeMS' value must be >= 0, actual value '-1.424534523412325e+16'",
altMessage: "BSON field 'maxTimeMS' value must be >= 0, actual value '-1.424534523412325e+16'",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"BigDouble": {
command: bson.D{
@@ -1009,7 +1129,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "BadValue",
Message: "9223372036854775807 value for maxTimeMS is out of range " + shareddata.Int32Interval,
},
altMessage: "1.797693134862316e+308 value for maxTimeMS is out of range",
altMessage: "1.797693134862316e+308 value for maxTimeMS is out of range",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"BigNegativeDouble": {
command: bson.D{
@@ -1023,7 +1144,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "Location51024",
Message: "BSON field 'maxTimeMS' value must be >= 0, actual value '-9223372036854775808'",
},
altMessage: "BSON field 'maxTimeMS' value must be >= 0, actual value '-1.797693134862316e+308'",
altMessage: "BSON field 'maxTimeMS' value must be >= 0, actual value '-1.797693134862316e+308'",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"NegativeInt32": {
command: bson.D{
@@ -1037,6 +1159,7 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "Location51024",
Message: "BSON field 'maxTimeMS' value must be >= 0, actual value '-1123123'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"MaxIntPlus": {
command: bson.D{
@@ -1050,7 +1173,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "BadValue",
Message: "2147483648 value for maxTimeMS is out of range " + shareddata.Int32Interval,
},
altMessage: "2147483648 value for maxTimeMS is out of range",
altMessage: "2147483648 value for maxTimeMS is out of range",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"Null": {
command: bson.D{
@@ -1064,6 +1188,7 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "BadValue",
Message: "maxTimeMS must be a number",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"String": {
command: bson.D{
@@ -1077,7 +1202,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "TypeMismatch",
Message: "BSON field 'aggregate.maxTimeMS' is the wrong type 'string', expected types '[long, int, decimal, double']",
},
altMessage: "BSON field 'aggregate.maxTimeMS' is the wrong type 'string', expected types '[long, int, decimal, double]'",
altMessage: "BSON field 'aggregate.maxTimeMS' is the wrong type 'string', expected types '[long, int, decimal, double]'",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"Array": {
command: bson.D{
@@ -1091,7 +1217,8 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "TypeMismatch",
Message: "BSON field 'aggregate.maxTimeMS' is the wrong type 'array', expected types '[long, int, decimal, double']",
},
altMessage: "BSON field 'aggregate.maxTimeMS' is the wrong type 'array', expected types '[long, int, decimal, double]'",
altMessage: "BSON field 'aggregate.maxTimeMS' is the wrong type 'array', expected types '[long, int, decimal, double]'",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
"Document": {
command: bson.D{
@@ -1105,12 +1232,17 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
Name: "TypeMismatch",
Message: "BSON field 'aggregate.maxTimeMS' is the wrong type 'object', expected types '[long, int, decimal, double']",
},
altMessage: "BSON field 'aggregate.maxTimeMS' is the wrong type 'object', expected types '[long, int, decimal, double]'",
altMessage: "BSON field 'aggregate.maxTimeMS' is the wrong type 'object', expected types '[long, int, decimal, double]'",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/354",
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
require.NotNil(t, tc.err, "err must not be nil")
@@ -1125,12 +1257,11 @@ func TestAggregateCommandMaxTimeMSErrors(t *testing.T) {
func TestAggregateCommandCursor(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
// the number of documents is set above the default batchSize of 101
// for testing unset batchSize returning default batchSize
arr, _ := GenerateDocuments(0, 110)
arr := GenerateDocuments(0, 110)
_, err := collection.InsertMany(ctx, arr)
require.NoError(t, err)
@@ -1138,10 +1269,10 @@ func TestAggregateCommandCursor(t *testing.T) {
pipeline any // optional, defaults to bson.A{}
cursor any // optional, nil to leave cursor unset
firstBatch primitive.A // optional, expected firstBatch
err *mongo.CommandError // optional, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
skip string // optional, skip test with a specified reason
firstBatch primitive.A // optional, expected firstBatch
err *mongo.CommandError // optional, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
failsForFerretDB string
}{
"Int": {
cursor: bson.D{{"batchSize", 1}},
@@ -1162,7 +1293,8 @@ func TestAggregateCommandCursor(t *testing.T) {
Name: "Location51024",
Message: "BSON field 'batchSize' value must be >= 0, actual value '-1'",
},
altMessage: "BSON field 'batchSize' value must be >= 0, actual value '-1'",
altMessage: "BSON field 'batchSize' value must be >= 0, actual value '-1'",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/349",
},
"DoubleZero": {
cursor: bson.D{{"batchSize", float64(0)}},
@@ -1175,6 +1307,7 @@ func TestAggregateCommandCursor(t *testing.T) {
Name: "Location51024",
Message: "BSON field 'batchSize' value must be >= 0, actual value '-1'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/349",
},
"DoubleFloor": {
cursor: bson.D{{"batchSize", 1.9}},
@@ -1188,7 +1321,7 @@ func TestAggregateCommandCursor(t *testing.T) {
Name: "TypeMismatch",
Message: "BSON field 'cursor.batchSize' is the wrong type 'bool', expected types '[long, int, decimal, double']",
},
altMessage: "BSON field 'aggregate.batchSize' is the wrong type 'bool', expected types '[long, int, decimal, double]'",
altMessage: "BSON field 'cursor.batchSize' is the wrong type 'bool', expected type 'number'",
},
"Unset": {
cursor: nil,
@@ -1225,9 +1358,13 @@ func TestAggregateCommandCursor(t *testing.T) {
firstBatch: arr[:6],
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
var pipeline any = bson.A{}
@@ -19,10 +19,10 @@ import (
"go.mongodb.org/mongo-driver/bson"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
func TestAggregateVariablesCompatRoot(t *testing.T) {
func TestAggregateCompatRootVariable(t *testing.T) {
t.Parallel()
providers := shareddata.AllProviders().Remove(shareddata.Composites)
@@ -32,7 +32,6 @@ func TestAggregateVariablesCompatRoot(t *testing.T) {
pipeline: bson.A{
bson.D{{"$addFields", bson.D{{"field", "$$ROOT"}}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/1413",
},
"GroupID": {
pipeline: bson.A{
@@ -125,7 +124,6 @@ func TestAggregateVariablesCompatRoot(t *testing.T) {
pipeline: bson.A{
bson.D{{"$set", bson.D{{"field", "$$ROOT"}}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/1413",
},
"Unwind": {
pipeline: bson.A{
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package oplog
// Package aggregation contains tests for the aggregate command.
package aggregation
import (
"testing"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestMain(m *testing.M) {
+392
View File
@@ -0,0 +1,392 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package auth
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/v2/integration"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestCreateUserCommand(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, nil)
ctx, db := s.Ctx, s.Collection.Database()
testCases := map[string]struct { //nolint:vet // for readability
payload bson.D
user string
expected bson.D
expectedMechanisms bson.A
expectedCredentialsComparable bson.D // field keys without values are compared for `salt`, `serverKey` and `storedKey` fields
err *mongo.CommandError
altMessage string
failsForFerretDB string
}{
"Empty": {
payload: bson.D{
{"createUser", ""},
{"roles", bson.A{}},
{"pwd", "password"},
},
user: "",
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
Message: "User document needs 'user' field to be non-empty",
},
altMessage: "createUser cannot be empty",
},
"EmptyPassword": {
payload: bson.D{
{"createUser", "empty_password_user"},
{"roles", bson.A{}},
{"pwd", ""},
},
user: "empty_password_user",
err: &mongo.CommandError{
Code: 50687,
Name: "Location50687",
Message: "Error preflighting UTF-8 conversion: U_STRING_NOT_TERMINATED_WARNING",
},
altMessage: "Password cannot be empty",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/934",
},
"BadPasswordValue": {
payload: bson.D{
{"createUser", "bad_password_user"},
{"roles", bson.A{}},
{"pwd", "pass\x00word"},
},
user: "bad_password_user",
err: &mongo.CommandError{
Code: 50692,
Name: "Location50692",
Message: "Error preflighting normalization: U_STRINGPREP_PROHIBITED_ERROR",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/941",
},
"BadPasswordType": {
payload: bson.D{
{"createUser", "bad_password_type_user"},
{"roles", bson.A{}},
{"pwd", true},
},
user: "bad_password_type_user",
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: "BSON field 'createUser.pwd' is the wrong type 'bool', expected type 'string'",
},
altMessage: "BSON field 'pwd' is the wrong type 'bool', expected type 'string'",
},
"AlreadyExists": {
payload: bson.D{
{"createUser", "should_already_exist"},
{"roles", bson.A{}},
{"pwd", "password"},
},
user: "should_already_exist",
err: &mongo.CommandError{
Code: 51003,
Name: "Location51003",
Message: fmt.Sprintf("User \"should_already_exist@%s\" already exists", db.Name()),
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/934",
},
"EmptyMechanism": {
payload: bson.D{
{"createUser", "empty_mechanism_user"},
{"roles", bson.A{}},
{"pwd", "password"},
{"mechanisms", bson.A{}},
},
user: "empty_mechanism_user",
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
Message: "mechanisms field must not be empty",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/913",
},
"BadAuthMechanism": {
payload: bson.D{
{"createUser", "success_user_with_scram_sha_1"},
{"roles", bson.A{}},
{"pwd", "password"},
{"mechanisms", bson.A{"BAD"}},
},
user: "success_user_with_scram_sha_1",
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
Message: "Unknown auth mechanism 'BAD'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/913",
},
"MissingPwdOrExternal": {
payload: bson.D{
{"createUser", "mising_pwd_or_external"},
{"roles", bson.A{}},
},
user: "mising_pwd_or_external",
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
Message: "Must provide a 'pwd' field for all user documents, except those with '$external' as the user's source db",
},
altMessage: "createUser, pwd and roles are required fields",
},
"Success": {
payload: bson.D{
{"createUser", "success_user"},
{"roles", bson.A{}},
{"pwd", "password"},
},
user: "success_user",
expectedMechanisms: bson.A{"SCRAM-SHA-256"},
expected: bson.D{
{"ok", float64(1)},
},
expectedCredentialsComparable: bson.D{
{"SCRAM-SHA-256", bson.D{
{"iterationCount", int32(15000)},
{Key: "salt"},
{Key: "storedKey"},
{Key: "serverKey"},
}},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"SuccessWithSCRAMSHA256": {
payload: bson.D{
{"createUser", "success_user_with_scram_sha_256"},
{"roles", bson.A{}},
{"pwd", "password"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
user: "success_user_with_scram_sha_256",
expectedMechanisms: bson.A{"SCRAM-SHA-256"},
expected: bson.D{
{"ok", float64(1)},
},
expectedCredentialsComparable: bson.D{
{"SCRAM-SHA-256", bson.D{
{"iterationCount", int32(15000)},
{Key: "salt"},
{Key: "storedKey"},
{Key: "serverKey"},
}},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"WithComment": {
payload: bson.D{
{"createUser", "with_comment_user"},
{"roles", bson.A{}},
{"pwd", "password"},
{"comment", "test string comment"},
},
user: "with_comment_user",
expectedMechanisms: bson.A{"SCRAM-SHA-256"},
expected: bson.D{
{"ok", float64(1)},
},
expectedCredentialsComparable: bson.D{
{"SCRAM-SHA-256", bson.D{
{"iterationCount", int32(15000)},
{Key: "salt"},
{Key: "storedKey"},
{Key: "serverKey"},
}},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"MissingRoles": {
payload: bson.D{
{"createUser", "missing_roles"},
{"pwd", "password"},
},
user: "missing_roles",
err: &mongo.CommandError{
Code: 40414,
Name: "Location40414",
Message: "BSON field 'createUser.roles' is missing but a required field",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/934",
},
"NilRoles": {
payload: bson.D{
{"createUser", "nil_roles"},
{"roles", nil},
{"pwd", "password"},
},
user: "nil_roles",
err: &mongo.CommandError{
Code: 10065,
Name: "Location10065",
Message: "invalid parameter: expected an object (roles)",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/934",
},
"InvalidRoles": {
payload: bson.D{
{"createUser", "invalid_roles"},
{"roles", "not-array"},
{"pwd", "password"},
},
user: "invalid_roles",
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: "BSON field 'createUser.roles' is the wrong type 'string', expected type 'array'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/934",
},
}
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
_ = db.RunCommand(ctx, bson.D{{"dropUser", "should_already_exist"}})
// The subtest "AlreadyExists" tries to create the following user, which should fail with an error that the user already exists.
// Here, we create the user for the very first time to populate the database.
err := db.RunCommand(ctx, bson.D{
{"createUser", "should_already_exist"},
{"roles", bson.A{}},
{"pwd", "password"},
}).Err()
require.NoError(t, err)
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(t, tc.failsForFerretDB)
}
tt.Parallel()
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
if tc.user != "should_already_exist" {
_ = db.RunCommand(ctx, bson.D{{"dropUser", tc.user}})
}
var res bson.D
err := db.RunCommand(ctx, tc.payload).Decode(&res)
if tc.err != nil {
integration.AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
return
}
require.NoError(t, err)
integration.AssertEqualDocuments(t, tc.expected, res)
err = db.RunCommand(ctx, bson.D{{"usersInfo", tc.user}, {"showCredentials", true}}).Decode(&res)
require.NoError(t, err)
var resComparable bson.D
for _, v := range res {
if v.Key != "users" {
resComparable = append(resComparable, v)
continue
}
var userComparable bson.D
for _, u := range v.Value.(bson.A)[0].(bson.D) {
switch u.Key {
case "userId":
uuid, ok := u.Value.(primitive.Binary)
assert.True(t, ok, "userId is not a primitive.Binary")
assert.Equal(t, bson.TypeBinaryUUID, uuid.Subtype, "uuid subtype")
assert.Equal(t, 16, len(uuid.Data), "UUID length")
userComparable = append(userComparable, bson.E{Key: u.Key})
case "credentials":
var credentials bson.D
for _, c := range u.Value.(bson.D) {
if c.Key != "SCRAM-SHA-256" {
continue
}
var mechanism bson.D
for _, m := range c.Value.(bson.D) {
switch m.Key {
case "salt", "serverKey", "storedKey":
assert.NotEmpty(t, m.Value.(string))
mechanism = append(mechanism, bson.E{Key: m.Key})
default:
mechanism = append(mechanism, m)
}
}
credentials = append(credentials, bson.E{Key: c.Key, Value: mechanism})
}
userComparable = append(userComparable, bson.E{Key: u.Key, Value: credentials})
case "mechanisms":
var mechanismsComparable bson.A
for _, m := range u.Value.(bson.A) {
if m != "SCRAM-SHA-256" {
continue
}
mechanismsComparable = append(mechanismsComparable, m)
}
userComparable = append(userComparable, bson.E{Key: u.Key, Value: mechanismsComparable})
default:
userComparable = append(userComparable, u)
}
}
resComparable = append(resComparable, bson.E{Key: v.Key, Value: bson.A{userComparable}})
}
expectedUsersInfo := bson.D{
{
"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.%s", db.Name(), tc.user)},
{Key: "userId"},
{"user", tc.user},
{"db", db.Name()},
{"credentials", tc.expectedCredentialsComparable},
{"roles", bson.A{}},
{"mechanisms", tc.expectedMechanisms},
},
},
},
{"ok", float64(1)},
}
integration.AssertEqualDocuments(t, expectedUsersInfo, resComparable)
})
}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package users
package auth
import (
"context"
@@ -24,26 +24,29 @@ import (
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/integration"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestDropAllUsersFromDatabase(t *testing.T) {
t.Parallel()
func TestDropAllUsersFromDatabaseCommand(tt *testing.T) {
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/864")
s := setup.SetupWithOpts(t, nil)
tt.Parallel()
s := setup.SetupWithOpts(tt, nil)
ctx := s.Ctx
db := s.Collection.Database()
client := db.Client()
quantity := 5 // Add some users to the database.
for i := 1; i <= quantity; i++ {
username := fmt.Sprintf("user_%d", i)
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
_ = db.RunCommand(ctx, bson.D{{"dropUser", username}})
err := db.RunCommand(ctx, bson.D{
{"createUser", fmt.Sprintf("user_%d", i)},
{"createUser", username},
{"roles", bson.A{}},
{"pwd", "password"},
}).Err()
@@ -61,7 +64,7 @@ func TestDropAllUsersFromDatabase(t *testing.T) {
assertDropAllUsersFromDatabase(t, ctx, db, 0)
}
func assertDropAllUsersFromDatabase(t *testing.T, ctx context.Context, db *mongo.Database, quantity int) {
func assertDropAllUsersFromDatabase(t testing.TB, ctx context.Context, db *mongo.Database, quantity int) {
t.Helper()
var res bson.D
@@ -71,23 +74,17 @@ func assertDropAllUsersFromDatabase(t *testing.T, ctx context.Context, db *mongo
require.NoError(t, err)
actual := integration.ConvertDocument(t, res)
actual.Remove("$clusterTime")
actual.Remove("operationTime")
expected := must.NotFail(types.NewDocument("n", int32(quantity), "ok", float64(1)))
testutil.AssertEqual(t, expected, actual)
expected := bson.D{{"n", int32(quantity)}, {"ok", float64(1)}}
integration.AssertEqualDocuments(t, expected, res)
var usersInfo bson.D
err = db.RunCommand(ctx, bson.D{{"usersInfo", 1}}).Decode(&usersInfo)
assert.NoError(t, err)
expectedUsersInfo := must.NotFail(types.NewDocument(
"users", new(types.Array),
"ok", float64(1),
))
actualUsersInfo := integration.ConvertDocument(t, usersInfo)
actualUsersInfo.Remove("$clusterTime")
actualUsersInfo.Remove("operationTime")
testutil.AssertEqual(t, expectedUsersInfo, actualUsersInfo)
expectedUsersInfo := bson.D{
{"users", bson.A{}},
{"ok", float64(1)},
}
integration.AssertEqualDocuments(t, expectedUsersInfo, usersInfo)
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package auth
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/v2/integration"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestDropUserCommand(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, nil)
ctx, db := s.Ctx, s.Collection.Database()
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
_ = db.RunCommand(ctx, bson.D{{"dropUser", "a_user"}})
errcmd := db.RunCommand(ctx, bson.D{ // avoid data race with and shadowing of err in parallel subtests below
{"createUser", "a_user"},
{"roles", bson.A{}},
{"pwd", "password"},
}).Err()
require.NoError(t, errcmd)
testCases := map[string]struct { //nolint:vet // for readability
username string
expected bson.D
err *mongo.CommandError
altMessage string
failsForFerretDB string
}{
"NotFound": {
username: "not_found_user",
err: &mongo.CommandError{
Code: 11,
Name: "UserNotFound",
Message: fmt.Sprintf("User 'not_found_user@%s' not found", db.Name()),
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/942",
},
"Success": {
username: "a_user",
expected: bson.D{
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(t, tc.failsForFerretDB)
}
tt.Parallel()
var res bson.D
err := db.RunCommand(ctx, bson.D{{"dropUser", tc.username}}).Decode(&res)
if tc.err != nil {
integration.AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
return
}
require.NoError(t, err)
integration.AssertEqualDocuments(t, tc.expected, res)
err = db.RunCommand(ctx, bson.D{{"usersInfo", bson.A{
bson.D{
{"user", tc.username},
{"db", db.Name()},
},
}}}).Decode(&res)
require.NoError(t, err)
expected := bson.D{
{"users", bson.A{}},
{"ok", float64(1)},
}
integration.AssertEqualDocuments(t, expected, res)
})
}
}
@@ -12,15 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
// Package auth contains tests for authentication and user management commands:
// - createUser;
// - dropAllUsersFromDatabase;
// - dropUser;
// - logout;
// - updateUser;
// - usersInfo.
package auth
import "strconv"
import (
"testing"
// Placeholder stores the number of the relevant placeholder of the query.
type Placeholder int
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
// Next increases the identifier value for the next variable in the PostgreSQL query.
func (p *Placeholder) Next() string {
*p++
return "$" + strconv.Itoa(int(*p))
func TestMain(m *testing.M) {
setup.Main(m)
}
+339
View File
@@ -0,0 +1,339 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package auth
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/v2/integration"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestUpdateUserCommand(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, nil)
ctx, db := s.Ctx, s.Collection.Database()
testCases := map[string]struct { //nolint:vet // for readability
username string
password string
mechanisms bson.A
updatePayload bson.D
expected bson.D
err *mongo.CommandError
altMessage string
failsForFerretDB string
}{
"MissingFields": {
username: "missing_fields",
password: "pass123654",
updatePayload: bson.D{
{"updateUser", "missing_fields"},
},
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
Message: "Must specify at least one field to update in updateUser",
},
altMessage: "updateUser and pwd are required fields",
},
"UserNotFound": {
username: "do_not_use",
password: "pass123654",
updatePayload: bson.D{
{"updateUser", "not_found"},
{"pwd", "password"},
},
err: &mongo.CommandError{
Code: 11,
Name: "UserNotFound",
Message: fmt.Sprintf("User not_found@%s not found", db.Name()),
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/945",
},
"EmptyUsername": {
username: "not_empty_username",
password: "password",
updatePayload: bson.D{
{"updateUser", ""},
{"pwd", "anewpassword"},
},
err: &mongo.CommandError{
Code: 11,
Name: "UserNotFound",
Message: fmt.Sprintf("User @%s not found", db.Name()),
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/945",
},
"EmptyPassword": {
username: "a_user_bad_password",
password: "password",
updatePayload: bson.D{
{"updateUser", "a_user_bad_password"},
{"pwd", ""},
},
err: &mongo.CommandError{
Code: 50687,
Name: "Location50687",
Message: "Error preflighting UTF-8 conversion: U_STRING_NOT_TERMINATED_WARNING",
},
altMessage: "Password cannot be empty",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/945",
},
"BadPasswordValue": {
username: "b_user_bad_password_value",
password: "password",
updatePayload: bson.D{
{"updateUser", "b_user_bad_password_value"},
{"pwd", true},
},
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: "BSON field 'updateUser.pwd' is the wrong type 'bool', expected type 'string'",
},
altMessage: "BSON field 'pwd' is the wrong type 'bool', expected type 'string'",
},
"BadPasswordType": {
username: "a_user_bad_password_type",
password: "password",
updatePayload: bson.D{
{"updateUser", "a_user_bad_password_type"},
{"pwd", true},
},
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: "BSON field 'updateUser.pwd' is the wrong type 'bool', expected type 'string'",
},
altMessage: "BSON field 'pwd' is the wrong type 'bool', expected type 'string'",
},
"SamePassword": {
username: "same_password_user",
password: "donotchange",
updatePayload: bson.D{
{"updateUser", "same_password_user"},
{"pwd", "donotchange"},
},
expected: bson.D{
{"users", bson.A{bson.D{
{"_id", fmt.Sprintf("%s.same_password_user", db.Name())},
{"user", "same_password_user"},
{"db", db.Name()},
{"roles", bson.A{}},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
}}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"PasswordChange": {
username: "a_user",
password: "password",
updatePayload: bson.D{
{"updateUser", "a_user"},
{"pwd", "anewpassword"},
},
expected: bson.D{
{"users", bson.A{bson.D{
{"_id", fmt.Sprintf("%s.a_user", db.Name())},
{"user", "a_user"},
{"db", db.Name()},
{"roles", bson.A{}},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
}}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"PasswordChangeWithSCRAMMechanism": {
username: "a_user_with_scram_mechanism",
password: "password",
mechanisms: bson.A{"SCRAM-SHA-256"},
updatePayload: bson.D{
{"updateUser", "a_user_with_scram_mechanism"},
{"pwd", "anewpassword"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
expected: bson.D{
{"users", bson.A{bson.D{
{"_id", fmt.Sprintf("%s.a_user_with_scram_mechanism", db.Name())},
{"user", "a_user_with_scram_mechanism"},
{"db", db.Name()},
{"roles", bson.A{}},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
}}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/946",
},
"PasswordChangeWithBadAuthMechanism": {
username: "a_user_with_mechanism_bad",
password: "password",
updatePayload: bson.D{
{"updateUser", "a_user_with_mechanism_bad"},
{"pwd", "anewpassword"},
{"mechanisms", bson.A{"BAD"}},
},
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
Message: "Unknown auth mechanism 'BAD'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/946",
},
"PasswordChangeWithRoles": {
username: "a_user_with_no_roles",
password: "password",
updatePayload: bson.D{
{"updateUser", "a_user_with_no_roles"},
{"roles", bson.A{}},
{"pwd", "anewpassword"},
},
expected: bson.D{
{"users", bson.A{bson.D{
{"_id", fmt.Sprintf("%s.a_user_with_no_roles", db.Name())},
{"user", "a_user_with_no_roles"},
{"db", db.Name()},
{"roles", bson.A{}},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
}}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/944",
},
"WithComment": {
username: "another_user",
password: "password",
updatePayload: bson.D{
{"updateUser", "another_user"},
{"pwd", "anewpassword"},
{"comment", "test string comment"},
},
expected: bson.D{
{"users", bson.A{bson.D{
{"_id", fmt.Sprintf("%s.another_user", db.Name())},
{"user", "another_user"},
{"db", db.Name()},
{"roles", bson.A{}},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
}}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"InvalidRoles": {
username: "invalid_roles",
password: "password",
updatePayload: bson.D{
{"updateUser", "invalid_roles"},
{"roles", "not-array"},
{"pwd", "password"},
},
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: "BSON field 'updateUser.roles' is the wrong type 'string', expected type 'array'",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/944",
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(t, tc.failsForFerretDB)
}
tt.Parallel()
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
_ = db.RunCommand(ctx, bson.D{{"dropUser", tc.username}})
createPayload := bson.D{
{"createUser", tc.username},
{"roles", bson.A{}},
{"pwd", tc.password},
}
if tc.mechanisms != nil {
createPayload = append(createPayload, bson.E{Key: "mechanisms", Value: tc.mechanisms})
}
err := db.RunCommand(ctx, createPayload).Err()
require.NoError(t, err)
var res bson.D
err = db.RunCommand(ctx, tc.updatePayload).Decode(&res)
if tc.err != nil {
integration.AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
return
}
require.NoError(t, err)
integration.AssertEqualDocuments(t, bson.D{{"ok", float64(1)}}, res)
err = db.RunCommand(ctx, bson.D{{"usersInfo", tc.username}}).Decode(&res)
require.NoError(t, err)
for i, v := range res {
if v.Key != "users" {
continue
}
var filteredUser bson.D
for _, u := range v.Value.(bson.A)[0].(bson.D) {
switch u.Key {
case "userId":
uuid, ok := u.Value.(primitive.Binary)
assert.True(t, ok, "userId is not a primitive.Binary")
assert.Equal(t, bson.TypeBinaryUUID, uuid.Subtype, "uuid subtype")
assert.Equal(t, 16, len(uuid.Data), "UUID length")
case "mechanisms":
var mechanismsComparable bson.A
for _, m := range u.Value.(bson.A) {
if m != "SCRAM-SHA-256" {
continue
}
mechanismsComparable = append(mechanismsComparable, m)
}
filteredUser = append(filteredUser, bson.E{Key: u.Key, Value: mechanismsComparable})
default:
filteredUser = append(filteredUser, u)
}
}
res[i].Value = bson.A{filteredUser}
}
integration.AssertEqualDocuments(t, tc.expected, res)
})
}
}
+743
View File
@@ -0,0 +1,743 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed 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.
package auth
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
"github.com/FerretDB/FerretDB/v2/integration"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
// createUser creates a bson.D command payload to create an user with the given username and password.
func createUser(username, password string) bson.D {
return bson.D{
{"createUser", username},
{"roles", bson.A{}},
{"pwd", password},
}
}
func TestUsersInfoCommand(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, nil)
ctx, collection := s.Ctx, s.Collection
client := collection.Database().Client()
dbPrefix := testutil.DatabaseName(t)
dbNames := struct {
A string
B string
C string
D string
E string
}{
A: dbPrefix,
B: dbPrefix + "_example",
C: dbPrefix + "_few",
D: dbPrefix + "_another",
E: dbPrefix + "allbackends",
}
dbToUsers := []struct {
dbName string
payloads []bson.D
}{
{
dbName: dbNames.A,
payloads: []bson.D{
createUser("one", "pwd1"),
createUser("two", "pwd2"),
},
},
{
dbName: dbNames.B,
payloads: []bson.D{
createUser("a", "password1"),
createUser("b", "password2"),
createUser("c", "password3"),
},
},
{
dbName: dbNames.C,
payloads: []bson.D{
createUser("i", "password1"),
createUser("j", "password2"),
},
},
{
dbName: dbNames.D,
payloads: []bson.D{
createUser("singleuser", "123456"),
},
},
{
dbName: dbNames.E,
payloads: []bson.D{
{
{"createUser", "WithSCRAMSHA256"},
{"roles", bson.A{}},
{"pwd", "pwd1"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
},
},
}
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
testUsernames := map[string]string{
"one": dbNames.A,
"two": dbNames.A,
"a": dbNames.B,
"b": dbNames.B,
"c": dbNames.B,
"i": dbNames.C,
"j": dbNames.C,
"singleuser": dbNames.D,
"WithSCRAMSHA256": dbNames.E,
}
for username, dbName := range testUsernames {
db := client.Database(dbName)
_ = db.RunCommand(ctx, bson.D{{"dropUser", username}})
}
for _, inserted := range dbToUsers {
db := client.Database(inserted.dbName)
t.Cleanup(func() {
db.RunCommand(ctx, bson.D{{"dropAllUsersFromDatabase", 1}})
})
for _, payload := range inserted.payloads {
err := db.RunCommand(ctx, payload).Err()
require.NoError(t, err)
}
}
testCases := map[string]struct { //nolint:vet // for readability
dbName string
payload bson.D
expectedComparable bson.D // field keys without values are compared for `userId`, `salt`, `serverKey` and `storedKey` fields
err *mongo.CommandError
altMessage string
failsForFerretDB string
}{
"NoUserFound": {
dbName: "no_users",
payload: bson.D{
{"usersInfo", int64(1)},
},
expectedComparable: bson.D{
{"users", bson.A{}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"Nil": {
dbName: dbNames.A,
payload: bson.D{
{"usersInfo", nil},
},
err: &mongo.CommandError{
Code: 2,
Message: "UserName must be either a string or an object",
Name: "BadValue",
},
altMessage: "Unusupported value for usersInfo",
},
"UnknownField": {
dbName: dbNames.A,
payload: bson.D{
// Note: if user is passed here, this test will fail only on MongoDB.
{"usersInfo", bson.D{{"foo", "bar"}}},
},
err: &mongo.CommandError{
Code: 2,
Message: "UserName contains an unknown field named: 'foo",
Name: "BadValue",
},
altMessage: "UserName must contain a field named: user",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"Default": {
dbName: dbNames.A,
payload: bson.D{
{"usersInfo", "one"},
},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.one", dbNames.A)},
{Key: "userId"},
{"user", "one"},
{"db", dbNames.A},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/961
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/962
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"WithSCRAMSHA256": {
dbName: dbNames.E,
payload: bson.D{
{"usersInfo", "WithSCRAMSHA256"},
{"showCredentials", true},
},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.WithSCRAMSHA256", dbNames.E)},
{Key: "userId"},
{"user", "WithSCRAMSHA256"},
{"db", dbNames.E},
{"credentials", bson.D{
{"SCRAM-SHA-256", bson.D{
{"iterationCount", int32(15000)},
{Key: "salt"},
{Key: "storedKey"},
{Key: "serverKey"},
}},
}},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"FromSameDatabase": {
dbName: dbNames.B,
payload: bson.D{{
"usersInfo", bson.A{
"a", "b",
},
}},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.a", dbNames.B)},
{Key: "userId"},
{"user", "a"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.b", dbNames.B)},
{Key: "userId"},
{"user", "b"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"FromSameDatabaseWithMissingUser": {
dbName: dbNames.B,
payload: bson.D{{
"usersInfo", bson.A{
"a", "b", "missing",
},
}},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.a", dbNames.B)},
{Key: "userId"},
{"user", "a"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.b", dbNames.B)},
{Key: "userId"},
{"user", "b"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"FromAnotherDatabase": {
dbName: dbNames.D,
payload: bson.D{{
"usersInfo", bson.D{
{"user", "one"},
{"db", dbNames.A},
},
}},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.one", dbNames.A)},
{Key: "userId"},
{"user", "one"},
{"db", dbNames.A},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/864",
},
"BadType": {
dbName: dbNames.B,
payload: bson.D{{
"usersInfo", true,
}},
err: &mongo.CommandError{
Code: 2,
Message: "UserName must be either a string or an object",
Name: "BadValue",
},
altMessage: "Unusupported value for usersInfo",
},
"BadTypeUsername": {
dbName: dbNames.D,
payload: bson.D{{
"usersInfo", bson.D{
{"user", 123},
{"db", dbNames.B},
},
}},
err: &mongo.CommandError{
Code: 2,
Message: "UserName must contain a string field named: user. But, has type int",
Name: "BadValue",
},
altMessage: fmt.Sprintf("Unsupported value specified for db : %s", dbNames.B),
},
"BadTypeDB": {
dbName: dbNames.D,
payload: bson.D{{
"usersInfo", bson.D{
{"user", "one"},
{"db", 123},
},
}},
err: &mongo.CommandError{
Code: 2,
Message: "UserName must contain a string field named: db. But, has type int",
Name: "BadValue",
},
altMessage: "UserName must contain a string field named: db. But, has type int32",
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/964",
},
"FromOthersMultipleDatabases": {
dbName: dbNames.C,
payload: bson.D{{
"usersInfo", bson.A{
bson.D{
{"user", "one"},
{"db", dbNames.A},
},
bson.D{
{"user", "i"},
{"db", dbNames.C},
},
},
}},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.i", dbNames.C)},
{Key: "userId"},
{"user", "i"},
{"db", dbNames.C},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.one", dbNames.A)},
{Key: "userId"},
{"user", "one"},
{"db", dbNames.A},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"FromMixed": {
dbName: dbNames.C,
payload: bson.D{{
"usersInfo", bson.A{
bson.D{
{"user", "one"},
{"db", dbNames.A},
},
"i",
},
}},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.i", dbNames.C)},
{Key: "userId"},
{"user", "i"},
{"db", dbNames.C},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.one", dbNames.A)},
{Key: "userId"},
{"user", "one"},
{"db", dbNames.A},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"SingleDatabaseInt": {
dbName: dbNames.B,
payload: bson.D{
{"usersInfo", int32(1)},
},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.a", dbNames.B)},
{Key: "userId"},
{"user", "a"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.b", dbNames.B)},
{Key: "userId"},
{"user", "b"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.c", dbNames.B)},
{Key: "userId"},
{"user", "c"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/939",
},
"SingleDatabaseLong": {
dbName: "no_users",
payload: bson.D{
{"usersInfo", int64(1)},
},
expectedComparable: bson.D{
{"users", bson.A{}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"SingleDatabaseFloat": {
dbName: "no_users",
payload: bson.D{
{"usersInfo", float64(1)},
},
expectedComparable: bson.D{
{"users", bson.A{}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"Multiple": {
dbName: dbNames.B,
payload: bson.D{
{"usersInfo", bson.A{
bson.D{{"user", "b"}, {"db", dbNames.B}},
bson.D{{"user", "c"}, {"db", dbNames.B}},
}},
},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.b", dbNames.B)},
{Key: "userId"},
{"user", "b"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.c", dbNames.B)},
{Key: "userId"},
{"user", "c"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"EmptyUsers": {
dbName: dbNames.B,
payload: bson.D{{
"usersInfo", bson.A{},
}},
err: &mongo.CommandError{
Code: 2,
Message: "$and/$or/$nor must be a nonempty array",
Name: "BadValue",
},
altMessage: "Unusupported value for usersInfo",
},
"MissingDBFieldName": {
dbName: dbNames.B,
payload: bson.D{
{"usersInfo", bson.A{
bson.D{{"user", "missing_db"}},
}},
},
err: &mongo.CommandError{
Code: 2,
Message: "UserName must contain a field named: db",
Name: "BadValue",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"MissingUserFieldName": {
dbName: dbNames.B,
payload: bson.D{
{"usersInfo", bson.A{
bson.D{{"db", dbNames.B}},
}},
},
err: &mongo.CommandError{
Code: 2,
Message: "UserName must contain a field named: user",
Name: "BadValue",
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
"ForAllDBs": {
dbName: dbNames.B,
payload: bson.D{
{"usersInfo", bson.D{
{"forAllDBs", true},
}},
},
expectedComparable: bson.D{
{"users", bson.A{
bson.D{
{"_id", fmt.Sprintf("%s.WithSCRAMSHA256", dbNames.E)},
{Key: "userId"},
{"user", "WithSCRAMSHA256"},
{"db", dbNames.E},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.a", dbNames.B)},
{Key: "userId"},
{"user", "a"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.b", dbNames.B)},
{Key: "userId"},
{"user", "b"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.c", dbNames.B)},
{Key: "userId"},
{"user", "c"},
{"db", dbNames.B},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.i", dbNames.C)},
{Key: "userId"},
{"user", "i"},
{"db", dbNames.C},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.j", dbNames.C)},
{Key: "userId"},
{"user", "j"},
{"db", dbNames.C},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.one", dbNames.A)},
{Key: "userId"},
{"user", "one"},
{"db", dbNames.A},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.singleuser", dbNames.D)},
{Key: "userId"},
{"user", "singleuser"},
{"db", dbNames.D},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
bson.D{
{"_id", fmt.Sprintf("%s.two", dbNames.A)},
{Key: "userId"},
{"user", "two"},
{"db", dbNames.A},
{Key: "roles"},
{"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}},
},
}},
{"ok", float64(1)},
},
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/963",
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
tt.Parallel()
var res bson.D
err := client.Database(tc.dbName).RunCommand(ctx, tc.payload).Decode(&res)
if tc.err != nil {
integration.AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
return
}
require.NoError(t, err)
var resComparable bson.D
for _, v := range res {
if v.Key != "users" {
resComparable = append(resComparable, v)
continue
}
var usersComparable bson.A
for _, u := range v.Value.(bson.A) {
var userComparable bson.D
var userCreatedByOtherTests bool
for _, field := range u.(bson.D) {
switch field.Key {
case "userId":
uuid, ok := field.Value.(primitive.Binary)
assert.True(t, ok, "userId is not a primitive.Binary")
assert.Equal(t, bson.TypeBinaryUUID, uuid.Subtype, "uuid subtype")
assert.Equal(t, 16, len(uuid.Data), "UUID length")
userComparable = append(userComparable, bson.E{Key: field.Key})
case "credentials":
var credentials bson.D
for _, c := range field.Value.(bson.D) {
var mechanism bson.D
for _, m := range c.Value.(bson.D) {
switch m.Key {
case "salt", "serverKey", "storedKey":
assert.NotEmpty(t, m.Value.(string))
mechanism = append(mechanism, bson.E{Key: m.Key})
default:
mechanism = append(mechanism, m)
}
}
credentials = append(credentials, bson.E{Key: c.Key, Value: mechanism})
}
userComparable = append(userComparable, bson.E{Key: field.Key, Value: credentials})
case "user":
// use username to check if user was created by this test
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/864
_, ok := testUsernames[field.Value.(string)]
if !ok {
userCreatedByOtherTests = true
}
userComparable = append(userComparable, field)
case "roles":
userComparable = append(userComparable, bson.E{Key: field.Key})
default:
userComparable = append(userComparable, field)
}
}
if userCreatedByOtherTests {
continue
}
usersComparable = append(usersComparable, userComparable)
}
resComparable = append(resComparable, bson.E{Key: v.Key, Value: usersComparable})
}
integration.AssertEqualDocuments(t, tc.expectedComparable, resComparable)
})
}
}
File diff suppressed because it is too large. Load diff
+235 -103
View File
@@ -27,16 +27,18 @@ import (
"go.mongodb.org/mongo-driver/mongo/options"
"go.opentelemetry.io/otel"
"github.com/FerretDB/FerretDB/internal/util/observability"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/v2/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
func TestMostCommandsAreCaseSensitive(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
func TestCommandCaseSensitive(tt *testing.T) {
tt.Parallel()
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/9")
ctx, collection := setup.Setup(tt)
db := collection.Database()
@@ -75,7 +77,7 @@ func TestFindNothing(t *testing.T) {
// FindOne sets limit parameter to 1, Find leaves it unset.
err := collection.FindOne(ctx, bson.D{}).Decode(&doc)
require.Equal(t, mongo.ErrNoDocuments, err)
require.Equal(t, mongo.ErrNoDocuments, err, "actual: %s", err)
assert.Equal(t, bson.D(nil), doc)
}
@@ -104,7 +106,7 @@ func TestInsertFind(t *testing.T) {
}
}
func TestOtelComment(t *testing.T) {
func TestFindOtelComment(t *testing.T) {
ctx, collection := setup.Setup(t, shareddata.Scalars)
ctx, span := otel.Tracer("").Start(ctx, "TestOtelComment")
@@ -175,6 +177,7 @@ func TestUpdateCommentMethod(t *testing.T) {
func TestUpdateCommentQuery(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.Scalars)
name := collection.Database().Name()
@@ -220,6 +223,7 @@ func TestDeleteCommentMethod(t *testing.T) {
func TestDeleteCommentQuery(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.Scalars)
name := collection.Database().Name()
@@ -239,7 +243,7 @@ func TestDeleteCommentQuery(t *testing.T) {
assert.Equal(t, expected, res)
}
func TestEmptyKey(t *testing.T) {
func TestFindEmptyKey(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
@@ -259,7 +263,7 @@ func TestEmptyKey(t *testing.T) {
assert.Equal(t, expected, actual)
}
func TestCollectionName(t *testing.T) {
func TestCreateCollection(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
@@ -267,12 +271,19 @@ func TestCollectionName(t *testing.T) {
collectionName300 := strings.Repeat("aB", 150)
collectionName235 := strings.Repeat("a", 235)
cases := map[string]struct {
// use short database name to stay within 255 bytes namespace limit for using long collection name
dbName := "short-db"
t.Cleanup(func() {
require.NoError(t, collection.Database().Client().Database(dbName).Drop(ctx))
})
testCases := map[string]struct {
collection string // collection name, defaults to empty string
err *mongo.CommandError // optional, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
skip string // optional, skip test with a specified reason
err *mongo.CommandError // optional, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
failsForFerretDB string
}{
"TooLongForBothDBs": {
collection: collectionName300,
@@ -280,14 +291,16 @@ func TestCollectionName(t *testing.T) {
Name: "InvalidNamespace",
Code: 73,
Message: fmt.Sprintf(
"Fully qualified namespace is too long. Namespace: TestCollectionName.%s Max: 255",
"Fully qualified namespace is too long. Namespace: %s.%s Max: 255",
dbName,
collectionName300,
),
},
altMessage: fmt.Sprintf("Invalid collection name: %s", collectionName300),
},
"LongEnough": {
collection: collectionName235,
collection: collectionName235,
failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/380",
},
"Short": {
collection: "a",
@@ -311,7 +324,7 @@ func TestCollectionName(t *testing.T) {
err: &mongo.CommandError{
Name: "InvalidNamespace",
Code: 73,
Message: "Invalid namespace specified 'TestCollectionName.'",
Message: fmt.Sprintf("Invalid namespace specified '%s.'", dbName),
},
altMessage: "Invalid collection name: ",
},
@@ -351,21 +364,20 @@ func TestCollectionName(t *testing.T) {
"Capital": {
collection: "A",
},
"Sqlite": {
collection: "sqlite_",
},
}
for name, tc := range cases {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
tt.Parallel()
var t testing.TB = tt
if tc.failsForFerretDB != "" {
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
t.Parallel()
db := collection.Database().Client().Database(dbName)
err := collection.Database().CreateCollection(ctx, tc.collection)
err := db.CreateCollection(ctx, tc.collection)
if tc.err != nil {
AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
return
@@ -373,11 +385,11 @@ func TestCollectionName(t *testing.T) {
assert.NoError(t, err)
names, err := collection.Database().ListCollectionNames(ctx, bson.D{})
names, err := db.ListCollectionNames(ctx, bson.D{})
require.NoError(t, err)
assert.Contains(t, names, tc.collection)
newCollection := collection.Database().Collection(tc.collection)
newCollection := db.Collection(tc.collection)
doc := bson.D{{"_id", "item"}}
_, err = newCollection.InsertOne(ctx, doc)
@@ -389,14 +401,13 @@ func TestCollectionName(t *testing.T) {
}
}
func TestDatabaseName(t *testing.T) {
func TestCreateCollectionDatabaseName(t *testing.T) {
t.Parallel()
t.Run("NoErr", func(t *testing.T) {
ctx, collection := setup.Setup(t)
for name, tc := range map[string]struct {
db string // database name, defaults to empty string
skip string // optional, skip test with a specified reason
db string // database name, defaults to empty string
}{
"Dash": {
db: "--",
@@ -404,9 +415,6 @@ func TestDatabaseName(t *testing.T) {
"Underscore": {
db: "__",
},
"Sqlite": {
db: "sqlite_",
},
"Number": {
db: "0prefix",
},
@@ -414,12 +422,7 @@ func TestDatabaseName(t *testing.T) {
db: strings.Repeat("a", 63),
},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
t.Parallel()
// there is no explicit command to create database, so create collection instead
@@ -437,12 +440,11 @@ func TestDatabaseName(t *testing.T) {
dbName64 := strings.Repeat("a", 64)
cases := map[string]struct {
testCases := map[string]struct {
db string // database name, defaults to empty string
err *mongo.CommandError // required, expected error from MongoDB
altMessage string // optional, alternative error message for FerretDB, ignored if empty
skip string // optional, skip test with a specified reason
}{
"TooLongForBothDBs": {
db: dbName64,
@@ -451,15 +453,16 @@ func TestDatabaseName(t *testing.T) {
Code: 73,
Message: "db name must be at most 63 characters, found: 64",
},
altMessage: fmt.Sprintf("Invalid namespace specified '%s.TestDatabaseName-Err'", dbName64),
altMessage: "database name is too long",
},
"WithASlash": {
db: "/",
err: &mongo.CommandError{
Name: "InvalidNamespace",
Code: 73,
Message: `Invalid namespace specified '/.TestDatabaseName-Err'`,
Message: `Invalid namespace specified '/.TestCreateCollectionDatabaseName-Err'`,
},
altMessage: "Database / has an invalid character /",
},
"WithABackslash": {
@@ -467,25 +470,27 @@ func TestDatabaseName(t *testing.T) {
err: &mongo.CommandError{
Name: "InvalidNamespace",
Code: 73,
Message: `Invalid namespace specified '\.TestDatabaseName-Err'`,
Message: `Invalid namespace specified '\.TestCreateCollectionDatabaseName-Err'`,
},
altMessage: `Database \ has an invalid character \`,
},
"WithADollarSign": {
db: "name_with_a-$",
err: &mongo.CommandError{
Name: "InvalidNamespace",
Code: 73,
Message: `Invalid namespace: name_with_a-$.TestDatabaseName-Err`,
Message: `Invalid namespace: name_with_a-$.TestCreateCollectionDatabaseName-Err`,
},
altMessage: `Invalid namespace specified 'name_with_a-$.TestDatabaseName-Err'`,
altMessage: "Database name_with_a-$ has an invalid character $",
},
"WithSpace": {
db: "data base",
err: &mongo.CommandError{
Name: "InvalidNamespace",
Code: 73,
Message: `Invalid namespace specified 'data base.TestDatabaseName-Err'`,
Message: `Invalid namespace specified 'data base.TestCreateCollectionDatabaseName-Err'`,
},
altMessage: "Database data base has an invalid character ",
},
"WithDot": {
db: "database.test",
@@ -494,17 +499,12 @@ func TestDatabaseName(t *testing.T) {
Code: 73,
Message: `'.' is an invalid character in a db name: database.test`,
},
altMessage: `Invalid namespace specified 'database.test.TestDatabaseName-Err'`,
altMessage: "Database database.test has an invalid character .",
},
}
for name, tc := range cases {
name, tc := name, tc
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
t.Parallel()
require.NotNil(t, tc.err, "err must not be nil")
@@ -517,28 +517,19 @@ func TestDatabaseName(t *testing.T) {
})
}
func TestDebugError(t *testing.T) {
func TestDebugCommandErrors(t *testing.T) {
setup.SkipForMongoDB(t, "FerretDB-specific command")
t.Parallel()
ctx, collection := setup.Setup(t)
db := collection.Database()
// TODO https://github.com/FerretDB/FerretDB/issues/2412
t.Run("ValidationError", func(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
db := collection.Database()
err := db.RunCommand(ctx, bson.D{{"debugError", bson.D{{"NaN", math.NaN()}}}}).Err()
require.ErrorContains(t, err, "socket was unexpectedly closed")
})
t.Run("LazyError", func(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
db := collection.Database()
err := db.RunCommand(ctx, bson.D{{"debugError", "lazy error"}}).Err()
expected := mongo.CommandError{
Code: 1,
@@ -553,9 +544,6 @@ func TestDebugError(t *testing.T) {
t.Run("OtherError", func(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
db := collection.Database()
err := db.RunCommand(ctx, bson.D{{"debugError", "other error"}}).Err()
expected := mongo.CommandError{
Code: 1,
@@ -574,23 +562,15 @@ func TestPingCommand(t *testing.T) {
ctx, collection := setup.Setup(t)
db := collection.Database()
expected := ConvertDocument(t, bson.D{{"ok", float64(1)}})
t.Run("Multiple", func(t *testing.T) {
t.Parallel()
for i := 0; i < 5; i++ {
res := db.RunCommand(ctx, bson.D{{"ping", int32(1)}})
var actualRes bson.D
err := res.Decode(&actualRes)
var res bson.D
err := db.RunCommand(ctx, bson.D{{"ping", int32(1)}}).Decode(&res)
require.NoError(t, err)
actual := ConvertDocument(t, actualRes)
actual.Remove("$clusterTime")
actual.Remove("operationTime")
testutil.AssertEqual(t, expected, actual)
AssertEqualDocuments(t, bson.D{{"ok", float64(1)}}, res)
}
})
@@ -599,33 +579,29 @@ func TestPingCommand(t *testing.T) {
dbName := "NonExistentDatabase"
expectedDatabases, err := db.Client().ListDatabases(ctx, bson.D{{"name", dbName}})
require.NoError(t, err)
require.Empty(t, expectedDatabases.Databases)
res := db.Client().Database(dbName).RunCommand(ctx, bson.D{{"ping", int32(1)}})
var actualRes bson.D
err = res.Decode(&actualRes)
list, err := db.Client().ListDatabases(ctx, bson.D{{"name", dbName}})
require.NoError(t, err)
actual := ConvertDocument(t, actualRes)
actual.Remove("$clusterTime")
actual.Remove("operationTime")
for _, dbSpec := range list.Databases {
require.NotEqual(t, dbSpec.Name, dbName)
}
expected := ConvertDocument(t, bson.D{{"ok", float64(1)}})
testutil.AssertEqual(t, expected, actual)
// Ensure that we don't create database on ping
// This also means that no collection is created during ping.
actualDatabases, err := db.Client().ListDatabases(ctx, bson.D{{"name", dbName}})
var res bson.D
err = db.Client().Database(dbName).RunCommand(ctx, bson.D{{"ping", int32(1)}}).Decode(&res)
require.NoError(t, err)
require.Empty(t, actualDatabases.Databases)
AssertEqualDocuments(t, bson.D{{"ok", float64(1)}}, res)
list, err = db.Client().ListDatabases(ctx, bson.D{{"name", dbName}})
require.NoError(t, err)
for _, dbSpec := range list.Databases {
require.NotEqual(t, dbSpec.Name, dbName)
}
})
}
func TestMutatingClientMetadata(t *testing.T) {
func TestHelloIsMasterCommandMutatingClientMetadata(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
@@ -684,3 +660,159 @@ func TestMutatingClientMetadata(t *testing.T) {
})
}
}
func TestInsertNullStrings(t *testing.T) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/420
t.Parallel()
ctx, collection := setup.Setup(t)
_, err := collection.InsertOne(ctx, bson.D{
{"_id", "document"},
{"a", string([]byte{0})},
})
require.NoError(t, err)
}
func TestInsertUpdateNestedArrays(t *testing.T) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/420
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.Scalars)
t.Run("Insert", func(t *testing.T) {
t.Parallel()
_, err := collection.InsertOne(ctx, bson.D{{"foo", bson.A{bson.A{"bar"}}}})
require.NoError(t, err)
})
t.Run("Update", func(t *testing.T) {
t.Parallel()
_, err := collection.UpdateOne(ctx, bson.D{}, bson.D{{"$set", bson.D{{"foo", bson.A{bson.A{"bar"}}}}}})
require.NoError(t, err)
})
}
func TestInsertUpdateFindNegativeZero(t *testing.T) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/420
t.Parallel()
ctx, collection := setup.Setup(t)
for name, tc := range map[string]struct {
insert bson.D
update bson.D
filter bson.D
}{
"Insert": {
insert: bson.D{{"_id", "1"}, {"v", math.Copysign(0.0, -1)}},
filter: bson.D{{"_id", "1"}},
},
"UpdateZeroMulNegative": {
insert: bson.D{{"_id", "zero"}, {"v", int32(0)}},
update: bson.D{{"$mul", bson.D{{"v", float64(-1)}}}},
filter: bson.D{{"_id", "zero"}},
},
"UpdateNegativeMulZero": {
insert: bson.D{{"_id", "negative"}, {"v", int64(-1)}},
update: bson.D{{"$mul", bson.D{{"v", float64(0)}}}},
filter: bson.D{{"_id", "negative"}},
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, err := collection.InsertOne(ctx, tc.insert)
require.NoError(t, err)
if tc.update != nil {
_, err = collection.UpdateOne(ctx, tc.filter, tc.update)
require.NoError(t, err)
}
var res bson.D
err = collection.FindOne(ctx, tc.filter).Decode(&res)
require.NoError(t, err)
expected := bson.D{
{"_id", tc.filter[0].Value},
{"v", math.Copysign(0.0, -1)},
}
AssertEqualDocuments(t, expected, res)
})
}
}
func TestInsertDocumentValidation(t *testing.T) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/420
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.Scalars)
t.Run("Insert", func(t *testing.T) {
t.Parallel()
for name, tc := range map[string]struct {
doc bson.D
}{
"DollarSign": {
doc: bson.D{{"$foo", "bar"}},
},
"DotSign": {
doc: bson.D{{"foo.bar", "baz"}},
},
"Infinity": {
doc: bson.D{{"foo", math.Inf(1)}},
},
"NegativeInfinity": {
doc: bson.D{{"foo", math.Inf(-1)}},
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, err := collection.InsertOne(ctx, tc.doc)
require.NoError(t, err)
})
}
})
t.Run("Update", func(t *testing.T) {
t.Parallel()
_, err := collection.UpdateOne(ctx, bson.D{}, bson.D{{"$set", bson.D{{"foo", bson.D{{"bar.baz", "qaz"}}}}}}, nil)
require.NoError(t, err)
})
}
func TestUpdateProduceInfinity(t *testing.T) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/420
t.Parallel()
ctx, collection := setup.Setup(t)
_, err := collection.InsertOne(ctx, bson.D{{"_id", "number"}, {"v", int32(42)}})
require.NoError(t, err)
_, err = collection.UpdateOne(ctx, bson.D{{"_id", "number"}}, bson.D{{"$mul", bson.D{{"v", math.MaxFloat64}}}})
require.NoError(t, err)
}
func TestCreateCollectionDatabaseNameNonLatin(t *testing.T) {
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/420
t.Parallel()
ctx, collection := setup.Setup(t)
dbName := "データベース"
cName := testutil.CollectionName(t)
err := collection.Database().Client().Database(dbName).CreateCollection(ctx, cName)
require.NoError(t, err)
err = collection.Database().Client().Database(dbName).Drop(ctx)
require.NoError(t, err)
}
+9 -18
View File
@@ -22,10 +22,10 @@ import (
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/internal/util/iterator"
"github.com/FerretDB/FerretDB/v2/internal/util/xiter"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/v2/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
func BenchmarkFind(b *testing.B) {
@@ -91,8 +91,6 @@ func BenchmarkReplaceOne(b *testing.B) {
})
ctx, collection := s.Ctx, s.Collection
// use the last document by the natural order to make non-pushdown path slower
cursor, err := collection.Find(ctx, bson.D{})
require.NoError(b, err)
@@ -135,8 +133,10 @@ func BenchmarkInsertMany(b *testing.B) {
ctx, collection := setup.Setup(b)
for _, provider := range shareddata.AllBenchmarkProviders() {
total, err := iterator.ConsumeCount(provider.NewIterator())
require.NoError(b, err)
var total int
for range provider.NewIter() {
total++
}
var batchSizes []int
for _, batchSize := range []int{1, 10, 100, 1000} {
@@ -152,16 +152,7 @@ func BenchmarkInsertMany(b *testing.B) {
for range b.N {
require.NoError(b, collection.Drop(ctx))
iter := provider.NewIterator()
for {
docs, err := iterator.ConsumeValuesN(iter, batchSize)
require.NoError(b, err)
if docs == nil {
break
}
for docs := range xiter.Chunk(provider.NewIter(), batchSize) {
insertDocs := make([]any, len(docs))
for i := range insertDocs {
insertDocs[i] = docs[i]
@@ -169,7 +160,7 @@ func BenchmarkInsertMany(b *testing.B) {
b.StartTimer()
_, err = collection.InsertMany(ctx, insertDocs)
_, err := collection.InsertMany(ctx, insertDocs)
require.NoError(b, err)
b.StopTimer()
@@ -15,320 +15,105 @@
package integration
import (
"math"
"math/rand/v2"
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/v2/integration/setup"
)
func TestCommandsAdministrationCompatCollStatsWithScale(t *testing.T) {
func TestListCollectionsCompat(t *testing.T) {
t.Parallel()
s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{shareddata.DocumentsDocuments},
AddNonExistentCollection: true,
})
ctx, targetCollection, compatCollection := s.Ctx, s.TargetCollections[0], s.CompatCollections[0]
for name, tc := range map[string]struct { //nolint:vet // for readability
scale any
resultType compatTestCaseResultType
}{
"scaleOne": {scale: int32(1)},
"scaleBig": {scale: int64(1000)},
"scaleMaxInt": {scale: math.MaxInt64},
"scaleZero": {scale: int32(0), resultType: emptyResult},
"scaleNegative": {scale: int32(-100), resultType: emptyResult},
"scaleFloat": {scale: 2.8},
"scaleFloatNegative": {scale: -2.8, resultType: emptyResult},
"scaleMinFloat": {scale: -math.MaxFloat64, resultType: emptyResult},
"scaleMaxFloat": {scale: math.MaxFloat64},
"scaleString": {
scale: "1",
resultType: emptyResult,
},
"scaleObject": {
scale: bson.D{{"a", 1}},
resultType: emptyResult,
},
"scaleNull": {scale: nil},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Helper()
t.Parallel()
var targetRes bson.D
targetCommand := bson.D{{"collStats", targetCollection.Name()}, {"scale", tc.scale}}
targetErr := targetCollection.Database().RunCommand(ctx, targetCommand).Decode(&targetRes)
var compatRes bson.D
compatCommand := bson.D{{"collStats", compatCollection.Name()}, {"scale", tc.scale}}
compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes)
if targetErr != nil {
t.Logf("Target error: %v", targetErr)
t.Logf("Compat error: %v", compatErr)
// error messages are intentionally not compared
AssertMatchesCommandError(t, compatErr, targetErr)
return
}
require.NoError(t, compatErr, "compat error; target returned no error")
targetDoc := ConvertDocument(t, targetRes)
compatDoc := ConvertDocument(t, compatRes)
targetFactor := must.NotFail(targetDoc.Get("scaleFactor"))
compatFactor := must.NotFail(compatDoc.Get("scaleFactor"))
assert.Equal(t, compatFactor, targetFactor)
})
}
}
func TestCommandsAdministrationCompatCollStatsCappedCollection(t *testing.T) {
t.Parallel()
s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{},
AddNonExistentCollection: true,
})
targetDB := s.TargetCollections[0].Database()
compatDB := s.CompatCollections[0].Database()
for name, tc := range map[string]struct {
sizeInBytes int64
maxDocuments int64
expectedSize int64
}{
"Size": {
sizeInBytes: 256,
expectedSize: 256,
},
"SizeRounded": {
sizeInBytes: 1000,
expectedSize: 1000,
},
"MaxDocuments": {
sizeInBytes: 1,
maxDocuments: 10,
expectedSize: 1,
},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()
cName := testutil.CollectionName(t) + name
opts := options.CreateCollection().SetCapped(true).SetSizeInBytes(tc.sizeInBytes)
if tc.maxDocuments > 0 {
opts.SetMaxDocuments(tc.maxDocuments)
}
targetErr := targetDB.CreateCollection(s.Ctx, cName, opts)
require.NoError(t, targetErr)
compatErr := compatDB.CreateCollection(s.Ctx, cName, opts)
require.NoError(t, compatErr)
command := bson.D{{"collStats", cName}}
var targetRes bson.D
targetErr = targetDB.RunCommand(s.Ctx, command).Decode(&targetRes)
require.NoError(t, targetErr)
var compatRes bson.D
compatErr = compatDB.RunCommand(s.Ctx, command).Decode(&compatRes)
require.NoError(t, compatErr)
targetDoc := ConvertDocument(t, targetRes)
compatDoc := ConvertDocument(t, compatRes)
assert.Equal(t, must.NotFail(compatDoc.Get("capped")), must.NotFail(targetDoc.Get("capped")))
// TODO https://github.com/FerretDB/FerretDB/issues/3582
assert.EqualValues(t, tc.expectedSize, must.NotFail(targetDoc.Get("maxSize")))
assert.EqualValues(t, must.NotFail(compatDoc.Get("maxSize")), must.NotFail(targetDoc.Get("maxSize")))
assert.EqualValues(t, must.NotFail(compatDoc.Get("max")), must.NotFail(targetDoc.Get("max")))
})
}
}
func TestCommandsAdministrationCompatDBStatsWithScale(t *testing.T) {
t.Parallel()
s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{shareddata.DocumentsDocuments},
AddNonExistentCollection: true,
})
ctx, targetCollection, compatCollection := s.Ctx, s.TargetCollections[0], s.CompatCollections[0]
for name, tc := range map[string]struct { //nolint:vet // for readability
scale any
resultType compatTestCaseResultType
}{
"scaleOne": {scale: int32(1)},
"scaleBig": {scale: int64(1000)},
"scaleFloat": {scale: 2.8},
"scaleNull": {scale: nil},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Helper()
t.Parallel()
var targetRes bson.D
targetCommand := bson.D{{"dbStats", int32(1)}, {"scale", tc.scale}}
targetErr := targetCollection.Database().RunCommand(ctx, targetCommand).Decode(&targetRes)
var compatRes bson.D
compatCommand := bson.D{{"dbStats", int32(1)}, {"scale", tc.scale}}
compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes)
if targetErr != nil {
t.Logf("Target error: %v", targetErr)
t.Logf("Compat error: %v", compatErr)
// error messages are intentionally not compared
AssertMatchesCommandError(t, compatErr, targetErr)
return
}
require.NoError(t, compatErr, "compat error; target returned no error")
targetDoc := ConvertDocument(t, targetRes)
compatDoc := ConvertDocument(t, compatRes)
targetFactor := must.NotFail(targetDoc.Get("scaleFactor"))
compatFactor := must.NotFail(compatDoc.Get("scaleFactor"))
assert.Equal(t, compatFactor, targetFactor)
})
}
}
func TestCommandsAdministrationCompatDBStatsFreeStorage(t *testing.T) {
t.Parallel()
s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{shareddata.DocumentsDocuments},
AddNonExistentCollection: true,
})
ctx, targetCollection, compatCollection := s.Ctx, s.TargetCollections[0], s.CompatCollections[0]
for name, tc := range map[string]struct { //nolint:vet // for readability
command bson.D // required, command to run
skip string // optional, skip test with a specified reason
}{
"Unset": {
command: bson.D{{"dbStats", int32(1)}},
},
"Int32Zero": {
command: bson.D{{"dbStats", int32(1)}, {"freeStorage", int32(0)}},
},
"Int32One": {
command: bson.D{{"dbStats", int32(1)}, {"freeStorage", int32(1)}},
},
"Int32Negative": {
command: bson.D{{"dbStats", int32(1)}, {"freeStorage", int32(-1)}},
},
"True": {
command: bson.D{{"dbStats", int32(1)}, {"freeStorage", true}},
},
"False": {
command: bson.D{{"dbStats", int32(1)}, {"freeStorage", false}},
},
"Nil": {
command: bson.D{{"dbStats", int32(1)}, {"freeStorage", nil}},
},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()
var targetRes bson.D
targetErr := targetCollection.Database().RunCommand(ctx, tc.command).Decode(&targetRes)
var compatRes bson.D
compatErr := compatCollection.Database().RunCommand(ctx, tc.command).Decode(&compatRes)
if targetErr != nil {
t.Logf("Target error: %v", targetErr)
t.Logf("Compat error: %v", compatErr)
// error messages are intentionally not compared
AssertMatchesCommandError(t, compatErr, targetErr)
return
}
require.NoError(t, compatErr, "compat error; target returned no error")
targetDoc := ConvertDocument(t, targetRes)
compatDoc := ConvertDocument(t, compatRes)
assert.Equal(t, compatDoc.Has("freeStorageSize"), targetDoc.Has("freeStorageSize"))
assert.Equal(t, compatDoc.Has("totalFreeStorageSize"), targetDoc.Has("totalFreeStorageSize"))
})
}
}
func TestCommandsAdministrationCompatListCollections(t *testing.T) {
t.Parallel()
ctx, targetCollections, compatCollections := setup.SetupCompat(t)
require.Greater(t, len(targetCollections), 2)
filterNames := make(bson.A, len(targetCollections))
for i, n := range targetCollections {
filterNames[i] = n.Name()
}
// We should remove shuffle there once it is implemented in the setup.
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/825
rand.Shuffle(len(filterNames), func(i, j int) { filterNames[i], filterNames[j] = filterNames[j], filterNames[i] })
filterNames = filterNames[:len(filterNames)-1]
require.NotEmpty(t, filterNames)
filter := bson.D{{
Key: "name", Value: bson.D{{
Key: "$in", Value: bson.A{
targetCollections[0].Name(),
targetCollections[len(targetCollections)-1].Name(),
},
"name", bson.D{{
"$in", filterNames,
}},
}}
target, err := targetCollections[0].Database().ListCollections(ctx, filter)
require.NoError(t, err)
var targetRes []bson.D
err = target.All(ctx, &targetRes)
require.NoError(t, err)
docTarget := ConvertDocument(t, targetRes[0])
compat, err := compatCollections[0].Database().ListCollections(ctx, filter)
require.NoError(t, err)
defer compat.Close(ctx)
var compatRes []bson.D
err = compat.All(ctx, &compatRes)
require.NoError(t, err)
docCompat := ConvertDocument(t, compatRes[0])
compatNames := make([]string, len(compatRes))
for i, doc := range compatRes {
compatNames[i] = doc.Map()["name"].(string)
}
require.True(t, slices.IsSorted(compatNames), "compat collections are not sorted")
target, err := targetCollections[0].Database().ListCollections(ctx, filter)
require.NoError(t, err)
defer target.Close(ctx)
var targetRes []bson.D
err = target.All(ctx, &targetRes)
require.NoError(t, err)
assert.Equal(t, target.RemainingBatchLength(), compat.RemainingBatchLength())
// Check idIndex
targetIDIndex := must.NotFail(docTarget.Get("idIndex"))
compatIDIndex := must.NotFail(docCompat.Get("idIndex"))
comparable := func(res []bson.D) []bson.D {
var resComparable []bson.D
assert.NotEmpty(t, targetIDIndex)
assert.Equal(t, targetIDIndex, compatIDIndex)
for _, doc := range res {
var docComparable bson.D
for _, field := range doc {
switch field.Key {
case "info":
info, ok := field.Value.(bson.D)
require.True(t, ok)
var infoComparable bson.D
for _, infoField := range info {
switch infoField.Key {
case "uuid":
uuid, uuidOk := infoField.Value.(primitive.Binary)
require.True(t, uuidOk)
assert.Equal(t, bson.TypeBinaryUUID, uuid.Subtype)
assert.Len(t, uuid.Data, 16)
infoComparable = append(infoComparable, bson.E{Key: infoField.Key, Value: primitive.Binary{}})
default:
infoComparable = append(infoComparable, infoField)
}
}
docComparable = append(docComparable, bson.E{Key: field.Key, Value: infoComparable})
default:
docComparable = append(docComparable, field)
}
}
resComparable = append(resComparable, docComparable)
}
return resComparable
}
AssertEqualDocumentsSlice(t, comparable(compatRes), comparable(targetRes))
}
Loaded 100 of 714 files, more files were not shown because too many files have changed in this diff. Show more