mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
6 Commits
claude/imp
...
ciro/test-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9172749b25 | ||
|
|
708e2cb3ec | ||
|
|
c8a6c6bd34 | ||
|
|
eb69db1a4c | ||
|
|
486ba80fe8 | ||
|
|
f585a453fd |
@@ -7,6 +7,7 @@ Docker Compose is a tool for defining and running multi-container Docker applica
|
||||
### Why Use Docker Compose Instead of Plain Docker?
|
||||
|
||||
**Without Docker Compose (the old way):**
|
||||
|
||||
```javascript
|
||||
// Each test file manages its own container
|
||||
const container = await Bun.spawn({
|
||||
@@ -21,6 +22,7 @@ const container = await Bun.spawn({
|
||||
```
|
||||
|
||||
**With Docker Compose (the new way):**
|
||||
|
||||
```javascript
|
||||
// All tests share managed containers
|
||||
const postgres = await dockerCompose.ensure("postgres_plain");
|
||||
@@ -34,26 +36,31 @@ const postgres = await dockerCompose.ensure("postgres_plain");
|
||||
## Benefits of This Setup
|
||||
|
||||
### 1. **Speed** 🚀
|
||||
|
||||
- Containers start once and stay running
|
||||
- Tests run 10-100x faster (no container startup overhead)
|
||||
- Example: PostgreSQL tests went from 30s to 3s
|
||||
|
||||
### 2. **No Port Conflicts** 🔌
|
||||
|
||||
- Docker Compose assigns random available ports automatically
|
||||
- No more "port already in use" errors
|
||||
- Multiple developers can run tests simultaneously
|
||||
|
||||
### 3. **Centralized Configuration** 📝
|
||||
|
||||
- All services defined in one `docker-compose.yml` file
|
||||
- Easy to update versions, add services, or change settings
|
||||
- No need to hunt through test files to find container configs
|
||||
|
||||
### 4. **Lazy Loading** 💤
|
||||
|
||||
- Services only start when actually needed
|
||||
- Running MySQL tests? Only MySQL starts
|
||||
- Saves memory and CPU
|
||||
|
||||
### 5. **Better CI/CD** 🔄
|
||||
|
||||
- Predictable, reproducible test environments
|
||||
- Same setup locally and in CI
|
||||
- Easy to debug when things go wrong
|
||||
@@ -63,6 +70,7 @@ const postgres = await dockerCompose.ensure("postgres_plain");
|
||||
### The Setup
|
||||
|
||||
1. **docker-compose.yml** - Defines all test services:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres_plain:
|
||||
@@ -70,11 +78,12 @@ services:
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- target: 5432 # Container's port
|
||||
published: 0 # 0 = let Docker pick a random port
|
||||
- target: 5432 # Container's port
|
||||
published: 0 # 0 = let Docker pick a random port
|
||||
```
|
||||
|
||||
2. **index.ts** - TypeScript helper for managing services:
|
||||
|
||||
```typescript
|
||||
// Start a service (if not already running)
|
||||
const info = await dockerCompose.ensure("postgres_plain");
|
||||
@@ -83,6 +92,7 @@ const info = await dockerCompose.ensure("postgres_plain");
|
||||
```
|
||||
|
||||
3. **Test Integration**:
|
||||
|
||||
```typescript
|
||||
import * as dockerCompose from "../../docker/index.ts";
|
||||
|
||||
@@ -90,7 +100,7 @@ test("database test", async () => {
|
||||
const pg = await dockerCompose.ensure("postgres_plain");
|
||||
const client = new PostgresClient({
|
||||
host: pg.host,
|
||||
port: pg.ports[5432], // Use the mapped port
|
||||
port: pg.ports[5432], // Use the mapped port
|
||||
});
|
||||
// ... run tests
|
||||
});
|
||||
@@ -98,22 +108,24 @@ test("database test", async () => {
|
||||
|
||||
## Available Services
|
||||
|
||||
| Service | Description | Ports | Special Features |
|
||||
|---------|-------------|-------|------------------|
|
||||
| **PostgreSQL** | | | |
|
||||
| `postgres_plain` | Basic PostgreSQL | 5432 | No auth required |
|
||||
| `postgres_tls` | PostgreSQL with TLS | 5432 | SSL certificates included |
|
||||
| `postgres_auth` | PostgreSQL with auth | 5432 | Username/password required |
|
||||
| **MySQL** | | | |
|
||||
| `mysql_plain` | Basic MySQL | 3306 | Root user, no password |
|
||||
| `mysql_native_password` | MySQL with legacy auth | 3306 | For compatibility testing |
|
||||
| `mysql_tls` | MySQL with TLS | 3306 | SSL certificates included |
|
||||
| **Redis/Valkey** | | | |
|
||||
| `redis_unified` | Redis with all features | 6379 (TCP), 6380 (TLS) | Persistence, Unix sockets, ACLs |
|
||||
| **S3/MinIO** | | | |
|
||||
| `minio` | S3-compatible storage | 9000 (API), 9001 (Console) | AWS S3 API testing |
|
||||
| **WebSocket** | | | |
|
||||
| `autobahn` | WebSocket test suite | 9002 | 517 conformance tests |
|
||||
| Service | Description | Ports | Special Features |
|
||||
| ---------------------------- | ----------------------- | -------------------------- | ------------------------------- |
|
||||
| **PostgreSQL** | | | |
|
||||
| `postgres_plain` | Basic PostgreSQL | 5432 | No auth required |
|
||||
| `postgres_tls` | PostgreSQL with TLS | 5432 | SSL certificates included |
|
||||
| `postgres_auth` | PostgreSQL with auth | 5432 | Username/password required |
|
||||
| **MySQL** | | | |
|
||||
| `mysql_plain` | Basic MySQL 8 | 3306 | Root user |
|
||||
| `mysql_plain_empty_password` | Basic MySQL 8 | 3306 | Root user, no password |
|
||||
| `mysql_native_password` | MySQL with legacy auth | 3306 | For compatibility testing |
|
||||
| `mysql_tls` | MySQL with TLS | 3306 | SSL certificates included |
|
||||
| `mysql_plain_9` | Basic MySQL 9 | 3306 | Root user |
|
||||
| **Redis/Valkey** | | | |
|
||||
| `redis_unified` | Redis with all features | 6379 (TCP), 6380 (TLS) | Persistence, Unix sockets, ACLs |
|
||||
| **S3/MinIO** | | | |
|
||||
| `minio` | S3-compatible storage | 9000 (API), 9001 (Console) | AWS S3 API testing |
|
||||
| **WebSocket** | | | |
|
||||
| `autobahn` | WebSocket test suite | 9002 | 517 conformance tests |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -193,27 +205,32 @@ This is different from the old approach where every test started and stopped its
|
||||
## Debugging
|
||||
|
||||
### View Running Services
|
||||
|
||||
```bash
|
||||
cd test/docker
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Check Service Logs
|
||||
|
||||
```bash
|
||||
docker-compose logs postgres_plain
|
||||
```
|
||||
|
||||
### Stop All Services
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Remove Everything (Including Data)
|
||||
|
||||
```bash
|
||||
docker-compose down -v # -v removes volumes too
|
||||
```
|
||||
|
||||
### Connection Issues?
|
||||
|
||||
```bash
|
||||
# Check if service is healthy
|
||||
docker-compose ps
|
||||
@@ -238,12 +255,14 @@ const pg = await dockerCompose.ensure("postgres_plain");
|
||||
### Persistent Data
|
||||
|
||||
Some services use volumes to persist data across container restarts:
|
||||
|
||||
- Redis: Uses volume for AOF persistence
|
||||
- PostgreSQL/MySQL: Can be configured with volumes if needed
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Control behavior with environment variables:
|
||||
|
||||
- `COMPOSE_PROJECT_NAME`: Prefix for container names (default: "bun-test-services")
|
||||
- `BUN_DOCKER_COMPOSE_PATH`: Override docker-compose.yml location
|
||||
|
||||
@@ -258,6 +277,7 @@ If you're migrating tests from direct Docker usage:
|
||||
5. **Cleanup**: Remove old Docker management code
|
||||
|
||||
Example migration:
|
||||
|
||||
```javascript
|
||||
// OLD
|
||||
const container = spawn(["docker", "run", "-d", "postgres"]);
|
||||
@@ -298,13 +318,13 @@ A: This tells Docker to pick any available port, preventing conflicts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| "Connection refused" | Service might still be starting. Add `waitTcp()` or increase timeout |
|
||||
| "Port already in use" | Another service using the port. Use dynamic ports (`published: 0`) |
|
||||
| "Container not found" | Run `docker-compose up -d SERVICE_NAME` manually |
|
||||
| Tests suddenly slow | Containers might have been stopped. Check with `docker-compose ps` |
|
||||
| "Permission denied" | Docker daemon might require sudo. Check Docker installation |
|
||||
| Problem | Solution |
|
||||
| --------------------- | -------------------------------------------------------------------- |
|
||||
| "Connection refused" | Service might still be starting. Add `waitTcp()` or increase timeout |
|
||||
| "Port already in use" | Another service using the port. Use dynamic ports (`published: 0`) |
|
||||
| "Container not found" | Run `docker-compose up -d SERVICE_NAME` manually |
|
||||
| Tests suddenly slow | Containers might have been stopped. Check with `docker-compose ps` |
|
||||
| "Permission denied" | Docker daemon might require sudo. Check Docker installation |
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -317,4 +337,4 @@ To add a new service:
|
||||
5. Add example test
|
||||
6. Submit PR
|
||||
|
||||
Remember: The goal is to make tests fast, reliable, and easy to run!
|
||||
Remember: The goal is to make tests fast, reliable, and easy to run!
|
||||
|
||||
@@ -77,6 +77,43 @@ services:
|
||||
|
||||
# MySQL Services
|
||||
mysql_plain:
|
||||
image: mysql:8.4
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
|
||||
MYSQL_DATABASE: bun_sql_test
|
||||
ports:
|
||||
- target: 3306
|
||||
published: 0
|
||||
protocol: tcp
|
||||
tmpfs:
|
||||
- /var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 1h # Effectively disable after startup
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
|
||||
mysql_plain_9:
|
||||
image: mysql:9
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
|
||||
MYSQL_DATABASE: bun_sql_test
|
||||
ports:
|
||||
- target: 3306
|
||||
published: 0
|
||||
protocol: tcp
|
||||
tmpfs:
|
||||
- /var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 1h # Effectively disable after startup
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
|
||||
mysql_plain_empty_password:
|
||||
image: mysql:8.4
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
@@ -97,7 +134,7 @@ services:
|
||||
mysql_native_password:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: bun
|
||||
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
|
||||
MYSQL_DATABASE: bun_sql_test
|
||||
MYSQL_ROOT_HOST: "%"
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
@@ -122,7 +159,7 @@ services:
|
||||
MYSQL_VERSION: 8.4
|
||||
image: bun-mysql-tls:local
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: bun
|
||||
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
|
||||
MYSQL_DATABASE: bun_sql_test
|
||||
ports:
|
||||
- target: 3306
|
||||
|
||||
@@ -11,6 +11,8 @@ export type ServiceName =
|
||||
| "postgres_tls"
|
||||
| "postgres_auth"
|
||||
| "mysql_plain"
|
||||
| "mysql_plain_empty_password"
|
||||
| "mysql_plain_9"
|
||||
| "mysql_native_password"
|
||||
| "mysql_tls"
|
||||
| "redis_plain"
|
||||
@@ -41,14 +43,14 @@ class DockerComposeHelper {
|
||||
private runningServices: Set<ServiceName> = new Set();
|
||||
|
||||
constructor(options: DockerComposeOptions = {}) {
|
||||
this.projectName = options.projectName ||
|
||||
this.projectName =
|
||||
options.projectName ||
|
||||
process.env.BUN_DOCKER_PROJECT_NAME ||
|
||||
process.env.COMPOSE_PROJECT_NAME ||
|
||||
"bun-test-services"; // Default project name for all test services
|
||||
"bun-test-services"; // Default project name for all test services
|
||||
|
||||
this.composeFile = options.composeFile ||
|
||||
process.env.BUN_DOCKER_COMPOSE_FILE ||
|
||||
join(__dirname, "docker-compose.yml");
|
||||
this.composeFile =
|
||||
options.composeFile || process.env.BUN_DOCKER_COMPOSE_FILE || join(__dirname, "docker-compose.yml");
|
||||
|
||||
// Verify the compose file exists
|
||||
const fs = require("fs");
|
||||
@@ -70,10 +72,7 @@ class DockerComposeHelper {
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
]);
|
||||
const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
@@ -151,12 +150,12 @@ class DockerComposeHelper {
|
||||
try {
|
||||
const socket = new net.Socket();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once('connect', () => {
|
||||
socket.once("connect", () => {
|
||||
socket.destroy();
|
||||
resolve();
|
||||
});
|
||||
socket.once('error', reject);
|
||||
socket.connect(port, '127.0.0.1');
|
||||
socket.once("error", reject);
|
||||
socket.connect(port, "127.0.0.1");
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
@@ -281,12 +280,14 @@ class DockerComposeHelper {
|
||||
break;
|
||||
|
||||
case "mysql_plain":
|
||||
case "mysql_plain_empty_password":
|
||||
case "mysql_plain_9":
|
||||
case "mysql_native_password":
|
||||
case "mysql_tls":
|
||||
env.MYSQL_HOST = info.host;
|
||||
env.MYSQL_PORT = info.ports[3306].toString();
|
||||
env.MYSQL_USER = "root";
|
||||
env.MYSQL_PASSWORD = service === "mysql_plain" ? "" : "bun";
|
||||
env.MYSQL_PASSWORD = service === "mysql_plain_empty_password" ? "" : "bun123456@#$%^&*()";
|
||||
env.MYSQL_DATABASE = "bun_sql_test";
|
||||
|
||||
if (info.tls) {
|
||||
@@ -449,7 +450,7 @@ export async function prepareImages(): Promise<void> {
|
||||
// Higher-level wrappers for tests
|
||||
export async function withPostgres(
|
||||
opts: { variant?: "plain" | "tls" | "auth" },
|
||||
fn: (info: ServiceInfo & { url: string }) => Promise<void>
|
||||
fn: (info: ServiceInfo & { url: string }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const variant = opts.variant || "plain";
|
||||
const serviceName = `postgres_${variant}` as ServiceName;
|
||||
@@ -467,7 +468,7 @@ export async function withPostgres(
|
||||
|
||||
export async function withMySQL(
|
||||
opts: { variant?: "plain" | "native_password" | "tls" },
|
||||
fn: (info: ServiceInfo & { url: string }) => Promise<void>
|
||||
fn: (info: ServiceInfo & { url: string }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const variant = opts.variant || "plain";
|
||||
const serviceName = `mysql_${variant}` as ServiceName;
|
||||
@@ -485,7 +486,7 @@ export async function withMySQL(
|
||||
|
||||
export async function withRedis(
|
||||
opts: { variant?: "plain" | "unified" },
|
||||
fn: (info: ServiceInfo & { url: string; tlsUrl?: string }) => Promise<void>
|
||||
fn: (info: ServiceInfo & { url: string; tlsUrl?: string }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const variant = opts.variant || "plain";
|
||||
const serviceName = `redis_${variant}` as ServiceName;
|
||||
@@ -502,7 +503,7 @@ export async function withRedis(
|
||||
}
|
||||
|
||||
export async function withMinio(
|
||||
fn: (info: ServiceInfo & { endpoint: string; accessKeyId: string; secretAccessKey: string }) => Promise<void>
|
||||
fn: (info: ServiceInfo & { endpoint: string; accessKeyId: string; secretAccessKey: string }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const info = await ensure("minio");
|
||||
|
||||
@@ -518,9 +519,7 @@ export async function withMinio(
|
||||
}
|
||||
}
|
||||
|
||||
export async function withAutobahn(
|
||||
fn: (info: ServiceInfo & { url: string }) => Promise<void>
|
||||
): Promise<void> {
|
||||
export async function withAutobahn(fn: (info: ServiceInfo & { url: string }) => Promise<void>): Promise<void> {
|
||||
const info = await ensure("autobahn");
|
||||
|
||||
try {
|
||||
@@ -531,4 +530,4 @@ export async function withAutobahn(
|
||||
} finally {
|
||||
// Services persist - no teardown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,10 +936,10 @@ export async function describeWithContainer(
|
||||
"postgres_tls": 5432,
|
||||
"postgres_auth": 5432,
|
||||
"mysql_plain": 3306,
|
||||
"mysql_native_password": 3306,
|
||||
"mysql_tls": 3306,
|
||||
"mysql:8": 3306, // Map mysql:8 to mysql_plain
|
||||
"mysql:9": 3306, // Map mysql:9 to mysql_native_password
|
||||
"mysql:9": 3306, // Map mysql:9 to mysql_plain_9
|
||||
"mysql_native_password": 3306, // mysql_native_password
|
||||
"redis_plain": 6379,
|
||||
"redis_unified": 6379,
|
||||
"minio": 9000,
|
||||
@@ -951,12 +951,12 @@ export async function describeWithContainer(
|
||||
// Map mysql:8 and mysql:9 based on environment variables
|
||||
let actualService = image;
|
||||
if (image === "mysql:8" || image === "mysql:9") {
|
||||
if (env.MYSQL_ROOT_PASSWORD === "bun") {
|
||||
actualService = "mysql_native_password"; // Has password "bun"
|
||||
} else if (env.MYSQL_ALLOW_EMPTY_PASSWORD === "yes") {
|
||||
actualService = "mysql_plain"; // No password
|
||||
if (env.MYSQL_ALLOW_EMPTY_PASSWORD === "yes") {
|
||||
actualService = "mysql_plain_empty_password"; // No password
|
||||
} else if (image === "mysql:9") {
|
||||
actualService = "mysql_plain_9";
|
||||
} else {
|
||||
actualService = "mysql_plain"; // Default to no password
|
||||
actualService = "mysql_plain";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,10 @@ const tests: {
|
||||
label: "mysql:8 with root user and password",
|
||||
database: {
|
||||
image: "mysql:8",
|
||||
env: {
|
||||
MYSQL_ROOT_PASSWORD: "bun",
|
||||
},
|
||||
},
|
||||
client: {
|
||||
user: "root",
|
||||
password: "bun",
|
||||
password: "bun123456@#$%^&*()",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,7 +12,8 @@ describeWithContainer(
|
||||
},
|
||||
container => {
|
||||
// Create getters that will be evaluated when the test runs
|
||||
const getUrl = () => `mysql://root:bun@${container.host}:${container.port}/bun_sql_test`;
|
||||
const getUrl = () =>
|
||||
`mysql://root:${encodeURIComponent("bun123456@#$%^&*()")}@${container.host}:${container.port}/bun_sql_test`;
|
||||
|
||||
test("should be able to connect with mysql_native_password auth plugin", async () => {
|
||||
console.log("Container info in test:", container);
|
||||
|
||||
@@ -13,7 +13,7 @@ describeWithContainer(
|
||||
container => {
|
||||
// Use a getter to avoid reading port/host at define time
|
||||
const getOptions = () => ({
|
||||
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
|
||||
url: `mysql://root:${encodeURIComponent("bun123456@#$%^&*()")}@${container.host}:${container.port}/bun_sql_test`,
|
||||
max: 1,
|
||||
bigint: true,
|
||||
});
|
||||
|
||||
@@ -24,9 +24,6 @@ if (isDockerEnabled()) {
|
||||
process.arch === "x64" && {
|
||||
name: "MySQL 9",
|
||||
image: "mysql:9",
|
||||
env: {
|
||||
MYSQL_ROOT_PASSWORD: "bun",
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -40,9 +37,9 @@ if (isDockerEnabled()) {
|
||||
},
|
||||
container => {
|
||||
let sql: SQL;
|
||||
const password = image.image === "mysql_plain" ? "" : "bun";
|
||||
const password = "bun123456@#$%^&*()";
|
||||
const getOptions = (): Bun.SQL.Options => ({
|
||||
url: `mysql://root:${password}@${container.host}:${container.port}/bun_sql_test`,
|
||||
url: `mysql://root:${encodeURIComponent(password)}@${container.host}:${container.port}/bun_sql_test`,
|
||||
max: 1,
|
||||
tls:
|
||||
image.name === "MySQL with TLS"
|
||||
|
||||
@@ -12,7 +12,7 @@ describeWithContainer(
|
||||
container => {
|
||||
// Use a getter to avoid reading port/host at define time
|
||||
const getOptions = () => ({
|
||||
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
|
||||
url: `mysql://root:${encodeURIComponent("bun123456@#$%^&*()")}@${container.host}:${container.port}/bun_sql_test`,
|
||||
max: 1,
|
||||
bigint: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user