Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
target
test-results
66 changes: 66 additions & 0 deletions .github/workflows/container-integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Container integration test

on:
workflow_dispatch:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: container-integration-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
clone-index-and-query:
name: Clone, index and query repository
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Check out GitDatabase
uses: actions/checkout@v4

- name: Prepare report directory
run: mkdir -p test-results

- name: Build containers and run integration test
run: >-
docker compose
-f docker/docker-compose.integration-test.yml
up
--build
--abort-on-container-exit
--exit-code-from integration-test
integration-test

- name: Publish integration test summary
if: always()
shell: bash
run: |
report="test-results/container-integration-test.md"
if [[ -f "${report}" ]]; then
cat "${report}" | tee -a "${GITHUB_STEP_SUMMARY}"
else
message="Integration test did not produce a report. Inspect the Compose logs above."
printf '## Container integration test\n\n%s\n' "${message}" | tee -a "${GITHUB_STEP_SUMMARY}"
fi

- name: Upload integration test report
if: always()
uses: actions/upload-artifact@v4
with:
name: container-integration-test-report
path: test-results/container-integration-test.md
if-no-files-found: warn

- name: Show container logs on failure
if: failure()
run: docker compose -f docker/docker-compose.integration-test.yml logs --no-color

- name: Stop integration infrastructure
if: always()
run: docker compose -f docker/docker-compose.integration-test.yml down --volumes --remove-orphans
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ target
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Container integration test output
/test-results/
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,29 @@ LIMIT 1;

---

## Teste de integração em containers

O teste de integração sobe um PostgreSQL isolado, clona
`DevComputaria/DevComputaria.Prompting`, executa `sync`, `hydrate-blobs` e
`search-index`, e valida o resultado com consultas SQL.

```bash
mkdir -p test-results
docker compose -f docker/docker-compose.integration-test.yml up \
--build \
--abort-on-container-exit \
--exit-code-from integration-test \
integration-test
docker compose -f docker/docker-compose.integration-test.yml down --volumes
```

O resultado detalhado é exibido no console e gravado em
`test-results/container-integration-test.md`. O workflow
`Container integration test` também publica esse relatório no Job Summary e
como artefato da execução.

---

## Documentação (MkDocs)

Este repositório possui documentação versionada em `docs/` com publicação automática para `gh-pages` via GitHub Actions.
Expand Down
36 changes: 36 additions & 0 deletions docker/Dockerfile.integration-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1

# Cargo.lock includes crates that require Rust 1.89 (for example, pgwire 0.38.3).
FROM rust:1.89-bookworm AS builder

WORKDIR /app

COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
COPY migrations ./migrations

RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=tmpfs,target=/app/target,size=4294967296 \
--mount=type=secret,id=ca_bundle,required=false \
if [ -f /run/secrets/ca_bundle ]; then \
CARGO_HTTP_CAINFO=/run/secrets/ca_bundle \
cargo build --locked --release -p gitbase-cli; \
else \
cargo build --locked --release -p gitbase-cli; \
fi \
&& cp /app/target/release/gitbase /tmp/gitbase

FROM debian:bookworm-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
git \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /tmp/gitbase /usr/local/bin/gitbase
COPY docker/run-container-integration-test.sh /usr/local/bin/run-container-integration-test

ENTRYPOINT ["/usr/local/bin/run-container-integration-test"]
35 changes: 35 additions & 0 deletions docker/docker-compose.integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: gitdatabase-integration-test

services:
postgres:
image: postgres:16-bookworm
environment:
POSTGRES_DB: gitbase
POSTGRES_USER: gitbase
POSTGRES_PASSWORD: gitbase
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitbase -d gitbase"]
interval: 2s
timeout: 3s
retries: 30
tmpfs:
- /var/lib/postgresql/data

integration-test:
image: gitdatabase-integration-test:local
build:
context: ..
dockerfile: docker/Dockerfile.integration-test
environment:
DATABASE_URL: postgres://gitbase:gitbase@postgres:5432/gitbase
EXPECTED_REPOSITORY_NAME: DevComputaria.Prompting
REPORT_FILE: /reports/container-integration-test.md
REPOSITORY_URL: https://github.com/DevComputaria/DevComputaria.Prompting.git
RUST_LOG: info
depends_on:
postgres:
condition: service_healthy
volumes:
- type: bind
source: ${GITDATABASE_REPORT_DIR:-../test-results}
target: /reports
157 changes: 157 additions & 0 deletions docker/run-container-integration-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
set -Eeuo pipefail

readonly REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/DevComputaria/DevComputaria.Prompting.git}"
readonly EXPECTED_REPOSITORY_NAME="${EXPECTED_REPOSITORY_NAME:-DevComputaria.Prompting}"
readonly REPOSITORY_ROOT="${REPOSITORY_ROOT:-/tmp/repositories}"
readonly REPOSITORY_PATH="${REPOSITORY_ROOT}/${EXPECTED_REPOSITORY_NAME}"
readonly REPORT_FILE="${REPORT_FILE:-/reports/container-integration-test.md}"
readonly DATABASE_URL="${DATABASE_URL:?DATABASE_URL is required}"

CURRENT_STAGE="initialization"
ASSERTIONS_PASSED=0
REPORT_FINALIZED=0

mkdir -p "$(dirname "${REPORT_FILE}")" "${REPOSITORY_ROOT}"

cat >"${REPORT_FILE}" <<EOF
# GitDatabase container integration test

- Repository: \`${REPOSITORY_URL}\`
- Expected repository name: \`${EXPECTED_REPOSITORY_NAME}\`
- Started at: \`$(date -u +'%Y-%m-%dT%H:%M:%SZ')\`

| Test | Result | Observed value |
| --- | --- | ---: |
EOF

report_row() {
local name="$1"
local result="$2"
local value="$3"
value="${value//$'\n'/ }"
value="${value//|/\\|}"
printf '| %s | %s | `%s` |\n' "${name}" "${result}" "${value}" >>"${REPORT_FILE}"
printf '[%s] %s (observed: %s)\n' "${result}" "${name}" "${value}"
}

finish() {
local exit_code=$?
trap - EXIT

if (( REPORT_FINALIZED == 0 )); then
if (( exit_code == 0 )); then
printf '\n**Result: PASS — %d SQL assertions passed.**\n' "${ASSERTIONS_PASSED}" >>"${REPORT_FILE}"
else
report_row "Integration pipeline" "FAIL" "stage=${CURRENT_STAGE}, exit=${exit_code}"
printf '\n**Result: FAIL — stopped during `%s`.**\n' "${CURRENT_STAGE}" >>"${REPORT_FILE}"
fi
printf '\n- Finished at: `%s`\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >>"${REPORT_FILE}"
REPORT_FINALIZED=1
fi

printf '\n========== Integration test report ==========\n'
cat "${REPORT_FILE}"
exit "${exit_code}"
}
trap finish EXIT

run_stage() {
CURRENT_STAGE="$1"
shift
printf '\n========== %s ==========\n' "${CURRENT_STAGE}"
"$@"
}

sql_scalar() {
local query="$1"
printf '%s\n' "${query}" | psql "${DATABASE_URL}" \
--no-psqlrc \
--set ON_ERROR_STOP=1 \
--set "repo_name=${EXPECTED_REPOSITORY_NAME}" \
--tuples-only \
--no-align | tr -d '[:space:]'
}

assert_greater_than_zero() {
local name="$1"
local query="$2"
local value
value="$(sql_scalar "${query}")"

if [[ "${value}" =~ ^[0-9]+$ ]] && (( value > 0 )); then
report_row "${name}" "PASS" "${value}"
ASSERTIONS_PASSED=$((ASSERTIONS_PASSED + 1))
else
report_row "${name}" "FAIL" "${value:-empty}"
return 1
fi
}

assert_equals() {
local name="$1"
local expected="$2"
local query="$3"
local value
value="$(sql_scalar "${query}")"

if [[ "${value}" == "${expected}" ]]; then
report_row "${name}" "PASS" "${value}"
ASSERTIONS_PASSED=$((ASSERTIONS_PASSED + 1))
else
report_row "${name}" "FAIL" "${value:-empty} (expected ${expected})"
return 1
fi
}

run_stage "clone repository" \
git clone --no-tags "${REPOSITORY_URL}" "${REPOSITORY_PATH}"

CLONED_COMMIT="$(git -C "${REPOSITORY_PATH}" rev-parse HEAD)"
report_row "Repository cloned" "PASS" "${CLONED_COMMIT}"

run_stage "database health and migrations" \
gitbase health --database-url "${DATABASE_URL}"

run_stage "repository metadata indexing" \
gitbase sync --database-url "${DATABASE_URL}" --repo-roots "${REPOSITORY_ROOT}"

run_stage "blob hydration" \
gitbase hydrate-blobs --database-url "${DATABASE_URL}" --repo-roots "${REPOSITORY_ROOT}"

run_stage "code search indexing" \
gitbase search-index --database-url "${DATABASE_URL}"

CURRENT_STAGE="SQL assertions"
printf '\n========== %s ==========\n' "${CURRENT_STAGE}"

assert_equals "Target repository indexed exactly once" "1" \
"SELECT count(*) FROM gitbase.repositories WHERE name = :'repo_name';"

assert_greater_than_zero "Git refs indexed" \
"SELECT count(*) FROM gitbase.refs r JOIN gitbase.repositories repo ON repo.id = r.repository_id WHERE repo.name = :'repo_name';"

assert_greater_than_zero "Git commits indexed" \
"SELECT count(*) FROM gitbase.commits c JOIN gitbase.repositories repo ON repo.id = c.repository_id WHERE repo.name = :'repo_name';"

assert_greater_than_zero "Repository files indexed" \
"SELECT count(*) FROM gitbase.files f JOIN gitbase.repositories repo ON repo.id = f.repository_id WHERE repo.name = :'repo_name';"

assert_greater_than_zero "Text blobs hydrated" \
"SELECT count(DISTINCT b.hash) FROM gitbase.blobs b JOIN gitbase.files f ON f.blob_hash = b.hash JOIN gitbase.repositories repo ON repo.id = f.repository_id WHERE repo.name = :'repo_name' AND b.content IS NOT NULL;"

assert_greater_than_zero "Code-search documents indexed" \
"SELECT count(DISTINCT ci.blob_hash) FROM gitbase.code_index ci JOIN gitbase.files f ON f.blob_hash = ci.blob_hash JOIN gitbase.repositories repo ON repo.id = f.repository_id WHERE repo.name = :'repo_name';"

assert_greater_than_zero "README.md query returns data" \
"SELECT count(*) FROM gitbase.files f JOIN gitbase.repositories repo ON repo.id = f.repository_id WHERE repo.name = :'repo_name' AND f.path = 'README.md';"

assert_greater_than_zero "search_code('prompt') returns matches" \
"SELECT count(*) FROM gitbase.search_code('prompt') result JOIN gitbase.repositories repo ON repo.id = result.repository_id WHERE repo.name = :'repo_name';"

assert_equals "No indexed file references a missing repository" "0" \
"SELECT count(*) FROM gitbase.files f LEFT JOIN gitbase.repositories repo ON repo.id = f.repository_id WHERE repo.id IS NULL;"

REPORT_FINALIZED=1
printf '\n**Result: PASS — %d SQL assertions passed.**\n' "${ASSERTIONS_PASSED}" >>"${REPORT_FILE}"
printf '\n- Finished at: `%s`\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >>"${REPORT_FILE}"
Loading