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 .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,6 @@ jobs:

- name: Check all go/toolchain directives match
run: CHECK=true make update-mod-go

- name: Check module zips exclude fixtures and ports
run: make check/modzip
77 changes: 77 additions & 0 deletions .github/workflows/conformance.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Conformance

# The Rust and JavaScript workflows run only when their own files change, but
# their fixture tests compare against values generated from River's Go code.
# This job closes that gap: when Go code, SQL queries the generator reads, or
# the generator itself changes, it regenerates the fixtures and runs only the
# port tests that read them. Keep both events' paths in sync.
on:
push:
branches:
- master
paths:
- ".github/workflows/conformance.yaml"
- "**.go"
- "**/go.mod"
- "**/go.sum"
- "conformance/**"
- "go.work"
- "riverdriver/**/*.sql"
pull_request:
paths:
- ".github/workflows/conformance.yaml"
- "**.go"
- "**/go.mod"
- "**/go.sum"
- "conformance/**"
- "go.work"
- "riverdriver/**/*.sql"

concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ${{ github.workflow }}-${{ github.ref }}

permissions:
contents: read

env:
# Keep the cross-run cache small; Cargo still reuses compiled dependencies.
CARGO_INCREMENTAL: "0"

jobs:
port_fixtures:
name: Port fixture tests
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- uses: actions/setup-go@v6
with:
go-version-file: go.work

- uses: dtolnay/rust-toolchain@stable
id: rust

- name: Cache Rust dependencies and build artifacts
uses: actions/cache@v5
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
rust/target
key: rust-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('rust/**/Cargo.toml', 'rust/Cargo.lock') }}
restore-keys: rust-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ steps.rust.outputs.cachekey }}-

- uses: ./.github/actions/setup-js

# Each target generates the fixtures before running its tests.
- name: Rust fixture tests
run: make test/rust/conformance

- name: JavaScript fixture tests
run: make test/js/conformance
11 changes: 11 additions & 0 deletions .github/workflows/js.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- ".github/actions/setup-js/**"
- ".github/workflows/js.yaml"
- "Makefile"
- "conformance/**"
- "js/**"
- "riverdriver/**/*.sql"
tags: ["v*"]
Expand All @@ -18,6 +19,7 @@ on:
- ".github/actions/setup-js/**"
- ".github/workflows/js.yaml"
- "Makefile"
- "conformance/**"
- "js/**"
- "riverdriver/**/*.sql"

Expand Down Expand Up @@ -182,6 +184,15 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

# Some unit tests read fixtures generated from River's Go
# implementation.
- uses: actions/setup-go@v6
with:
go-version-file: go.work

- run: make generate/fixtures
working-directory: .

# The unit tests import the other workspace packages through their
# build output, and worker threads run compiled handler modules.
- run: pnpm run build:all
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/rust.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths:
- ".github/workflows/rust.yaml"
- "Makefile"
- "conformance/**"
- "internal/cmd/syncrustmigrations/**"
- "riverdriver/riverpgxv5/migration/**"
- "riverdriver/riversqlite/migration/**"
Expand All @@ -17,6 +18,7 @@ on:
paths:
- ".github/workflows/rust.yaml"
- "Makefile"
- "conformance/**"
- "internal/cmd/syncrustmigrations/**"
- "riverdriver/riverpgxv5/migration/**"
- "riverdriver/riversqlite/migration/**"
Expand Down Expand Up @@ -108,6 +110,10 @@ jobs:
rust/target
key: rust-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('rust/**/Cargo.toml', 'rust/Cargo.lock') }}
restore-keys: rust-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ steps.rust.outputs.cachekey }}-
# Tests read fixtures generated from River's Go implementation.
- uses: actions/setup-go@v6
with:
go-version-file: go.work
- name: Check every target and feature
run: cargo check --manifest-path rust/Cargo.toml --workspace --all-targets --all-features --locked
- name: Unit, doc, and SQLite tests
Expand Down Expand Up @@ -150,6 +156,10 @@ jobs:
rust/target
key: rust-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('rust/**/Cargo.toml', 'rust/Cargo.lock') }}
restore-keys: rust-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ steps.rust.outputs.cachekey }}-
# Tests read fixtures generated from River's Go implementation.
- uses: actions/setup-go@v6
with:
go-version-file: go.work
- name: Create test database
run: PGPASSWORD=postgres createdb -h localhost -U postgres river_rust_test
- name: Rust unit, SQLite, and PostgreSQL tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.envrc
/conformance/testdata/
/go.work.sum
/internal/cmd/riverbench/riverbench
/river
Expand Down
33 changes: 33 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

SQLC ?= sqlc

.PHONY: check/modzip
check/modzip: ## Check that no Go module zip includes fixtures, testdata, or the Rust or JS ports
go run ./conformance/cmd/checkmodzip ./go.work

.PHONY: db/reset
db/reset: ## Drop, create, and migrate dev and test databases
db/reset: db/reset/dev
Expand All @@ -19,11 +23,18 @@ db/reset/test: ## Drop, create, and migrate test databases

.PHONY: generate
generate: ## Generate generated artifacts
generate: generate/fixtures
generate: generate/js-migrations
generate: generate/migrations
generate: generate/rust-migrations
generate: generate/sqlc

# Fixtures are written to conformance/testdata, which is ignored by Git. Port
# test targets that read them depend on this target.
.PHONY: generate/fixtures
generate/fixtures: ## Generate cross-language conformance fixtures from River's Go implementation
go run ./conformance/cmd/generatefixtures

.PHONY: generate/js-migrations
generate/js-migrations: ## Sync database migrations to JavaScript
pnpm -C js run generate:migrations
Expand Down Expand Up @@ -139,8 +150,17 @@ RUST_POSTGRES_TESTS_ENV = RUSTFLAGS="$$RUSTFLAGS --cfg river_postgres_tests" \
.PHONY: test/js
test/js: ## Run JavaScript unit tests
test/js: build/js
test/js: generate/fixtures
pnpm -C js run test

# Only the tests that compare JavaScript with fixtures generated from River's
# Go implementation, for checking a Go change against the port. They import
# sources directly and need no build.
.PHONY: test/js/conformance
test/js/conformance: ## Run JavaScript tests that check Go-generated conformance fixtures
test/js/conformance: generate/fixtures
pnpm -C js exec vitest run src/cron.test.ts src/runtime/completion-command.test.ts

# Integration tests use TEST_DATABASE_URL (default
# postgres://localhost:5432/river_test), migrated with
# `node js/cli/dist/bin.js migrate-up`.
Expand All @@ -151,6 +171,7 @@ test/js/integration: build/js

.PHONY: test/rust
test/rust: ## Run Rust unit and SQLite tests, plus PostgreSQL tests when RIVER_RUST_DATABASE_URL is set
test/rust: generate/fixtures
@if [ -n "$$RIVER_RUST_DATABASE_URL" ]; then \
cd rust && $(RUST_POSTGRES_TESTS_ENV) cargo test --workspace --all-features --locked; \
elif [ -n "$$CI" ]; then \
Expand All @@ -160,13 +181,22 @@ test/rust: ## Run Rust unit and SQLite tests, plus PostgreSQL tests when RIVER_R
cd rust && cargo test --workspace --features riverqueue/sqlite,riverqueue-migrate/sqlite --locked; \
fi

# Only the tests that compare Rust with fixtures generated from River's Go
# implementation, for checking a Go change against the port.
.PHONY: test/rust/conformance
test/rust/conformance: ## Run Rust tests that check Go-generated conformance fixtures
test/rust/conformance: generate/fixtures
cd rust && cargo test -p riverqueue --features chrono-tz --lib --test protocol_fixtures --locked

.PHONY: test/rust/postgres
test/rust/postgres: ## Run all Rust tests, including PostgreSQL integration tests (requires RIVER_RUST_DATABASE_URL)
test/rust/postgres: generate/fixtures
@test -n "$$RIVER_RUST_DATABASE_URL" || { echo "RIVER_RUST_DATABASE_URL is required" >&2; exit 1; }
cd rust && $(RUST_POSTGRES_TESTS_ENV) cargo test --workspace --all-features --locked

.PHONY: test/rust/sqlite
test/rust/sqlite: ## Run Rust unit, doc, and SQLite integration tests without a PostgreSQL database
test/rust/sqlite: generate/fixtures
cd rust && cargo test --workspace --features riverqueue/sqlite,riverqueue-migrate/sqlite --locked

.PHONY: doc/js
Expand Down Expand Up @@ -214,6 +244,9 @@ check/rust/package: ## Build and verify publishable crate archives without publi
cd rust && package_build_dir=$$(mktemp -d) && \
trap 'rm -rf "$$package_build_dir"' EXIT && \
CARGO_BUILD_BUILD_DIR="$$package_build_dir" cargo package --workspace --allow-dirty --locked
cd rust && for crate in riverqueue riverqueue-cli riverqueue-macros riverqueue-migrate riverqueue-test; do \
! cargo package --list --allow-dirty --locked -p $$crate | grep -E '(^|/)(tests|fixtures|testdata)/|\.json$$' | grep -vxF .cargo_vcs_info.json || exit 1; \
done

# The baseline is the latest published riverqueue-v* tag, and
# cargo-semver-checks infers the allowed change from the version bump. It
Expand Down
91 changes: 91 additions & 0 deletions conformance/cmd/checkmodzip/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Command checkmodzip fails if the module zip of any module in a Go workspace
// would include test fixtures or another language's port. It selects files
// with the same rules the Go module proxy uses, so it catches fixtures that a
// missing nested go.mod would otherwise publish with River. It reads the
// working tree, ignored files included, so it's exact on a clean checkout
// like CI's.
//
// Run it with a make target:
//
// make check/modzip
package main

import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"

"golang.org/x/mod/modfile"
"golang.org/x/mod/zip"
)

// conformanceModulePath is this command's own module. It holds the shared
// fixtures by design and is never published, so it isn't checked.
const conformanceModulePath = "github.com/riverqueue/river/conformance"

// disallowedPathPattern matches paths, relative to a module's root, that must
// never be published: fixture and testdata directories, JSON files, and the
// conformance, JavaScript, and Rust trees.
var disallowedPathPattern = regexp.MustCompile(`(^|/)(fixtures?|testdata)/|\.json$|^(conformance|js|rust)/`)

func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: checkmodzip <path to go.work>")
os.Exit(2)
}

if err := run(os.Args[1]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run(workFilename string) error {
workFileData, err := os.ReadFile(workFilename) //nolint:gosec // a developer-supplied go.work path
if err != nil {
return fmt.Errorf("error reading %s: %w", workFilename, err)
}

workFile, err := modfile.ParseWork(workFilename, workFileData, nil)
if err != nil {
return fmt.Errorf("error parsing %s: %w", workFilename, err)
}

var violations []error
for _, use := range workFile.Use {
dir := filepath.Join(filepath.Dir(workFilename), use.Path)

modFilename := filepath.Join(dir, "go.mod")
modFileData, err := os.ReadFile(modFilename) //nolint:gosec // a module directory listed in go.work
if err != nil {
return fmt.Errorf("error reading %s: %w", modFilename, err)
}
modulePath := modfile.ModulePath(modFileData)
if modulePath == conformanceModulePath {
continue
}

files, err := zip.CheckDir(dir)
if err != nil {
return fmt.Errorf("error checking module zip for %s: %w", modulePath, err)
}

for _, path := range files.Valid {
relPath, err := filepath.Rel(dir, path)
if err != nil {
return fmt.Errorf("error making %s relative to %s: %w", path, dir, err)
}
relPath = filepath.ToSlash(relPath)

if disallowedPathPattern.MatchString(relPath) {
violations = append(violations, fmt.Errorf("%s: module zip would include %s", modulePath, relPath))
}
}

fmt.Printf("%s: %d files\n", modulePath, len(files.Valid))
}

return errors.Join(violations...)
}
Loading
Loading