From bcbadb0deaa5470321dde4b374e88709ca5f6365 Mon Sep 17 00:00:00 2001 From: Dragos Varovici Date: Sat, 28 Mar 2026 11:24:54 +0200 Subject: [PATCH] Docs --- docs/banner.png | Bin 0 -> 67366 bytes docs/community-logos/github.svg | 4 + docs/community-logos/slack.svg | 10 + docs/community-logos/stackoverflow.svg | 5 + docs/community-logos/twitter.svg | 4 + docs/contributing.md | 11 + docs/contributing_docs.md | 69 ++++ docs/css/extra.css | 128 ++++++ docs/css/tc-header.css | 375 ++++++++++++++++++ docs/examples/index.md | 245 ++++++++++++ docs/favicon.ico | Bin 0 -> 15406 bytes docs/features/best_practices.md | 139 +++++++ docs/features/configuration.md | 79 ++++ docs/features/connection_strings.md | 143 +++++++ docs/features/creating_container.md | 118 ++++++ docs/features/creating_image.md | 53 +++ docs/features/garbage_collector.md | 100 +++++ docs/features/low_level_api.md | 76 ++++ docs/features/networking.md | 110 +++++ docs/features/wait/introduction.md | 161 ++++++++ docs/index.md | 91 +++++ docs/js/tc-header.js | 45 +++ docs/language-logos/dotnet.svg | 7 + docs/language-logos/go.svg | 10 + docs/language-logos/haskell.svg | 6 + docs/language-logos/java.svg | 17 + docs/language-logos/nodejs.svg | 5 + docs/language-logos/python.svg | 8 + docs/language-logos/ruby.svg | 125 ++++++ docs/language-logos/rust.svg | 57 +++ docs/logo.png | Bin 0 -> 21628 bytes docs/logo.svg | 92 +++++ docs/modules/index.md | 79 ++++ docs/modules/mongodb.md | 57 +++ docs/modules/mysql.md | 63 +++ docs/modules/postgres.md | 66 +++ docs/modules/redis.md | 60 +++ docs/quickstart/index.md | 102 +++++ docs/system_requirements/ci/dind_patterns.md | 52 +++ docs/system_requirements/ci/github_actions.md | 61 +++ docs/system_requirements/ci/gitlab_ci.md | 43 ++ docs/system_requirements/index.md | 48 +++ docs/test_frameworks/zig_test.md | 120 ++++++ docs/testcontainers-logo.svg | 22 + docs/theme/main.html | 10 + docs/theme/partials/header.html | 150 +++++++ docs/theme/partials/nav.html | 79 ++++ docs/theme/partials/tc-header.html | 157 ++++++++ 48 files changed, 3462 insertions(+) create mode 100644 docs/banner.png create mode 100644 docs/community-logos/github.svg create mode 100644 docs/community-logos/slack.svg create mode 100644 docs/community-logos/stackoverflow.svg create mode 100644 docs/community-logos/twitter.svg create mode 100644 docs/contributing.md create mode 100644 docs/contributing_docs.md create mode 100644 docs/css/extra.css create mode 100644 docs/css/tc-header.css create mode 100644 docs/examples/index.md create mode 100644 docs/favicon.ico create mode 100644 docs/features/best_practices.md create mode 100644 docs/features/configuration.md create mode 100644 docs/features/connection_strings.md create mode 100644 docs/features/creating_container.md create mode 100644 docs/features/creating_image.md create mode 100644 docs/features/garbage_collector.md create mode 100644 docs/features/low_level_api.md create mode 100644 docs/features/networking.md create mode 100644 docs/features/wait/introduction.md create mode 100644 docs/index.md create mode 100644 docs/js/tc-header.js create mode 100644 docs/language-logos/dotnet.svg create mode 100644 docs/language-logos/go.svg create mode 100644 docs/language-logos/haskell.svg create mode 100644 docs/language-logos/java.svg create mode 100644 docs/language-logos/nodejs.svg create mode 100644 docs/language-logos/python.svg create mode 100644 docs/language-logos/ruby.svg create mode 100644 docs/language-logos/rust.svg create mode 100644 docs/logo.png create mode 100644 docs/logo.svg create mode 100644 docs/modules/index.md create mode 100644 docs/modules/mongodb.md create mode 100644 docs/modules/mysql.md create mode 100644 docs/modules/postgres.md create mode 100644 docs/modules/redis.md create mode 100644 docs/quickstart/index.md create mode 100644 docs/system_requirements/ci/dind_patterns.md create mode 100644 docs/system_requirements/ci/github_actions.md create mode 100644 docs/system_requirements/ci/gitlab_ci.md create mode 100644 docs/system_requirements/index.md create mode 100644 docs/test_frameworks/zig_test.md create mode 100644 docs/testcontainers-logo.svg create mode 100644 docs/theme/main.html create mode 100644 docs/theme/partials/header.html create mode 100644 docs/theme/partials/nav.html create mode 100644 docs/theme/partials/tc-header.html diff --git a/docs/banner.png b/docs/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..88961b3e3f0c0520a48e91b06e353a73fce35594 GIT binary patch literal 67366 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 AC;$Ke literal 0 HcmV?d00001 diff --git a/docs/community-logos/github.svg b/docs/community-logos/github.svg new file mode 100644 index 0000000..d563c83 --- /dev/null +++ b/docs/community-logos/github.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/docs/community-logos/slack.svg b/docs/community-logos/slack.svg new file mode 100644 index 0000000..942265e --- /dev/null +++ b/docs/community-logos/slack.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/docs/community-logos/stackoverflow.svg b/docs/community-logos/stackoverflow.svg new file mode 100644 index 0000000..c330762 --- /dev/null +++ b/docs/community-logos/stackoverflow.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/docs/community-logos/twitter.svg b/docs/community-logos/twitter.svg new file mode 100644 index 0000000..ef3fbc6 --- /dev/null +++ b/docs/community-logos/twitter.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..5c3e5b0 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,11 @@ +# Contributing + +* Star the project on [GitHub](https://github.com/dragosv/testcontainers-zig) and help spread the word :) +* Join our [Slack Workspace](https://slack.testcontainers.org/) +* Post an issue if you find any bugs +* Contribute improvements or fixes using a Pull Request. If you're going to contribute, thank you! Please just be sure to: + * Discuss with the authors on an issue ticket prior to doing anything big. + * Follow the style, naming, and structure conventions of the rest of the project. + * Make commits atomic and easy to merge. + * Verify all tests are passing with `zig build test --summary all` (requires Docker running for integration tests). + * Let `zig fmt` handle formatting. diff --git a/docs/contributing_docs.md b/docs/contributing_docs.md new file mode 100644 index 0000000..02d900d --- /dev/null +++ b/docs/contributing_docs.md @@ -0,0 +1,69 @@ +# Contributing to Documentation + +The Testcontainers for Zig documentation lives in the `docs/` directory of the repository. + +## Structure + +``` +docs/ +├── index.md # Landing page +├── contributing.md # Contributing guide +├── contributing_docs.md # This file +├── quickstart/ +│ └── index.md # Getting started guide +├── features/ +│ ├── creating_container.md # ContainerRequest API +│ ├── creating_image.md # Image pulling and management +│ ├── networking.md # Ports and networks +│ ├── configuration.md # Docker host and configuration +│ ├── garbage_collector.md # Container cleanup patterns +│ ├── best_practices.md # Recommendations +│ ├── connection_strings.md # Connection string reference +│ ├── low_level_api.md # DockerClient access +│ └── wait/ +│ └── introduction.md # Wait strategies +├── modules/ +│ ├── index.md # Module overview +│ ├── postgres.md # PostgreSQL module +│ ├── mysql.md # MySQL module +│ ├── redis.md # Redis module +│ └── mongodb.md # MongoDB module +├── system_requirements/ +│ ├── index.md # Zig, Docker, and OS requirements +│ └── ci/ +│ ├── github_actions.md # GitHub Actions setup +│ ├── gitlab_ci.md # GitLab CI/CD setup +│ └── dind_patterns.md # Docker-in-Docker patterns +├── examples/ +│ └── index.md # Usage examples +└── test_frameworks/ + └── zig_test.md # Zig test integration patterns +``` + +## Guidelines + +- Use Zig code examples — not Swift, Go, Java, or other languages. +- All code examples must use the actual testcontainers-zig API (`ContainerRequest` structs, `DockerProvider`, etc.). +- Ensure examples are consistent with the actual API in `src/`. +- Use fenced code blocks with the `zig` language identifier. +- Follow the [testcontainers-go documentation](https://golang.testcontainers.org/) style: + - Each page should follow: Introduction → Usage example → Reference tables → Examples. + - Module pages use: Introduction → Adding dependency → Usage example → Module Reference → Examples. + - Use tables for API reference (parameters, methods, options). + - Use admonitions (`!!! tip`, `!!! warning`, `!!! note`) for callouts. + +## Adding a new page + +1. Create the Markdown file in the appropriate directory. +2. Follow the structure of existing pages in the same section. +3. Cross-link to related pages where appropriate. +4. Verify all code samples are consistent with the current API. + +## Adding a new module page + +When a new container module is added to `src/modules/`, create a corresponding documentation page in `docs/modules/`: + +1. Copy the structure from an existing module page (e.g., `postgres.md`). +2. Fill in: Introduction, Adding the dependency, Usage example, Module Reference (Options table, Container Methods table, Wait Strategy), and Examples. +3. Add the module to the table in `docs/modules/index.md`. +4. Cross-link from `docs/features/connection_strings.md` if it provides a connection string method. diff --git a/docs/css/extra.css b/docs/css/extra.css new file mode 100644 index 0000000..bf4db2a --- /dev/null +++ b/docs/css/extra.css @@ -0,0 +1,128 @@ +h1, h2, h3, h4, h5, h6 { + font-family: 'Rubik', sans-serif; +} + +[data-md-color-scheme="testcontainers"] { + --md-primary-fg-color: #00bac2; + --md-accent-fg-color: #361E5B; + --md-typeset-a-color: #0C94AA; + --md-primary-fg-color--dark: #291A3F; + --md-default-fg-color--lightest: #F2F4FE; + --md-footer-fg-color: #361E5B; + --md-footer-fg-color--light: #746C8F; + --md-footer-fg-color--lighter: #C3BEDE; + --md-footer-bg-color: #F7F9FD; + --md-footer-bg-color--dark: #F7F9FD; +} + +.card-grid { + display: grid; + gap: 10px; +} + +.tc-version { + font-size: 1.1em; + text-align: center; + margin: 0; +} + +@media (min-width: 680px) { + .card-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +body .card-grid-item { + display: flex; + align-items: center; + gap: 20px; + border: 1px solid #C3BEDE; + border-radius: 6px; + padding: 16px; + font-weight: 600; + color: #9991B5; + background: #F2F4FE; +} + +body .card-grid-item:hover, +body .card-grid-item:focus { + color: #9991B5; +} + +.card-grid-item[href] { + color: var(--md-primary-fg-color--dark); + background: transparent; +} + +.card-grid-item[href]:hover, +.card-grid-item[href]:focus { + background: #F2F4FE; + color: var(--md-primary-fg-color--dark); +} + +.community-callout-wrapper { + padding: 30px 10px 0 10px; +} + +.community-callout { + color: #F2F4FE; + background: linear-gradient(10.88deg, rgba(102, 56, 242, 0.4) 9.56%, #6638F2 100%), #291A3F; + box-shadow: 0px 20px 45px rgba(#9991B5, 0.75); + border-radius: 10px; + padding: 20px; +} + +.community-callout h2 { + font-size: 1.15em; + margin: 0 0 20px 0; + color: #F2F4FE; + text-align: center; +} + +.community-callout ul { + list-style: none; + padding: 0; + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 20px; + margin-bottom: 0; +} + +.community-callout a { + transition: opacity 0.2s ease; +} + +.community-callout a:hover { + opacity: 0.5; +} + +.community-callout a img { + height: 1.75em; + width: auto; + aspect-ratio: 1; +} + +@media (min-width: 1220px) { + .community-callout-wrapper { + padding: 40px 0 0; + } + + .community-callout h2 { + font-size: 1.25em; + } + + .community-callout a img { + height: 2em; + } +} + +@media (min-width: 1600px) { + .community-callout h2 { + font-size: 1.15em; + } + + .community-callout a img { + height: 1.75em; + } +} \ No newline at end of file diff --git a/docs/css/tc-header.css b/docs/css/tc-header.css new file mode 100644 index 0000000..122102b --- /dev/null +++ b/docs/css/tc-header.css @@ -0,0 +1,375 @@ + +:root { + --color-catskill: #F2F4FE; + --color-catskill-45: rgba(242, 244, 254, 0.45); + --color-mist: #E7EAFB; + --color-fog: #C3C7E6; + --color-smoke: #9991B5; + --color-smoke-75: rgba(153, 145, 181, 0.75); + --color-storm: #746C8F; + --color-topaz: #00BAC2; + --color-pacific: #17A6B2; + --color-teal: #027F9E; + --color-eggplant: #291A3F; + --color-plum: #361E5B; + +} + +#site-header { + color: var(--color-storm); + background: #fff; + font-family: 'Rubik', Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 1.5; + position: relative; + width: 100%; + z-index: 4; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 20px; +} + +body.tc-header-active #site-header { + z-index: 5; +} + +#site-header .brand { + display: flex; + justify-content: space-between; + gap: 20px; + width: 100%; +} + +#site-header .logo { + display: flex; +} + +#site-header .logo img, +#site-header .logo svg { + height: 30px; + width: auto; + max-width: 100%; +} + +#site-header #mobile-menu-toggle { + background: none; + border: none; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + color: var(--color-eggplant); + padding: 0; + margin: 0; + font-weight: 500; +} + +body.mobile-menu #site-header #mobile-menu-toggle { + color: var(--color-topaz); +} + +#site-header ul { + list-style: none; + padding: 0; + margin: 0; +} + +#site-header nav { + display: none; +} + +#site-header .menu-item { + display: flex; +} + +#site-header .menu-item button, +#site-header .menu-item a { + min-height: 30px; + display: flex; + gap: 6px; + align-items: center; + border: none; + background: none; + cursor: pointer; + padding: 0; + font-weight: 500; + color: var(--color-eggplant); + text-decoration: none; + font-size: 14px; + transition: color 0.2s ease; + white-space: nowrap; +} + +#site-header .menu-item button:hover, +#site-header .menu-item a:hover { + color: var(--color-topaz); +} + +#site-header .menu-item button .icon-external, +#site-header .menu-item a .icon-externa { + margin-left: auto; + opacity: .3; + flex-shrink: 0; +} + +#site-header .menu-item button .icon-caret, +#site-header .menu-item a .icon-caret { + opacity: .3; + height: 8px; +} + +#site-header .menu-item button .icon-slack, +#site-header .menu-item a .icon-slack, +#site-header .menu-item button .icon-github, +#site-header .menu-item a .icon-github { + height: 18px; +} + +#site-header .menu-item .menu-dropdown { + flex-direction: column; +} + +body #site-header .menu-item .menu-dropdown { + display: none; +} + +#site-header .menu-item.has-children.active .menu-dropdown { + display: flex; + z-index: 10; +} + +#site-header .menu-dropdown-item + .menu-dropdown-item { + border-top: 1px solid var(--color-mist); +} + +#site-header .menu-dropdown-item a { + display: flex; + gap: 10px; + align-items: center; + padding: 10px 20px; + font-weight: 500; + color: var(--color-eggplant); + text-decoration: none; + transition: + color 0.2s ease, + background 0.2s ease; +} + +#site-header .menu-dropdown-item a .icon-external { + margin-left: auto; + color: var(--color-fog); + flex-shrink: 0; + opacity: 1; +} + +#site-header .menu-dropdown-item a:hover { + background-color: var(--color-catskill-45); +} + +#site-header .menu-dropdown-item a:hover .icon-external { + color: var(--color-topaz); +} + +#site-header .menu-dropdown-item a img { + height: 24px; +} + +.md-header { + background-color: var(--color-catskill); + color: var(--color-eggplant); +} + +.md-header.md-header--shadow { + box-shadow: none; +} + +.md-header__inner.md-grid { + max-width: 100%; + padding: 1.5px 20px; +} + +[dir=ltr] .md-header__title { + margin: 0; +} + +.md-header__topic:first-child { + font-size: 16px; + font-weight: 500; + font-family: 'Rubik', Arial, Helvetica, sans-serif; +} + +.md-header__title.md-header__title--active .md-header__topic, +.md-header__title[data-md-state=active] .md-header__topic { + opacity: 1; + pointer-events: all; + transform: translateX(0); + transition: none; + z-index: 0; +} + +.md-header__topic a { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + transition: color .2s ease; +} + +.md-header__topic a:hover { + color: var(--color-topaz); +} + +div.md-header__source { + width: auto; +} + +div.md-source__repository { + max-width: 100%; +} + +.md-main { + padding: 0 12px; +} + +@media screen and (min-width: 60em) { + form.md-search__form { + background-color: #FBFBFF; + color: var(--color-storm); + } + + form.md-search__form:hover { + background-color: #fff; + } + + .md-search__input + .md-search__icon { + color: var(--color-plum); + } + + .md-search__input::placeholder { + color: var(--color-smoke); + } +} + +@media (min-width: 500px) { + #site-header { + font-size: 16px; + padding: 20px 40px; + } + #site-header .logo img, + #site-header .logo svg { + height: 48px; + } + + #site-header .menu-item button .icon-caret, + #site-header .menu-item a .icon-caret { + height: 10px; + } + + #site-header .menu-item button .icon-slack, + #site-header .menu-item a .icon-slack, + #site-header .menu-item button .icon-github, + #site-header .menu-item a .icon-github { + height: 24px; + } + + .md-header__inner.md-grid { + padding: 5px 40px; + } + + .md-main { + padding: 0 32px; + } +} + +@media (min-width: 1024px) { + #site-header #mobile-menu-toggle { + display: none; + } + + #site-header nav { + display: block; + } + + #site-header .menu { + display: flex; + justify-content: center; + gap: 30px; + } + + #site-header .menu-item { + align-items: center; + position: relative; + } + + #site-header .menu-item button, + #site-header .menu-item a { + min-height: 48px; + gap: 8px; + font-size: 16px; + } + + #site-header .menu-item .menu-dropdown { + position: absolute; + top: 100%; + right: -8px; + border: 1px solid var(--color-mist); + border-radius: 6px; + background: #fff; + box-shadow: 0px 30px 35px var(--color-smoke-75); + min-width: 200px; + } +} + + +@media (max-width: 1023px) { + #site-header { + flex-direction: column; + } + + body.mobile-tc-header-active #site-header { + z-index: 5; + } + + body.mobile-menu #site-header nav { + display: flex; + } + + #site-header nav { + position: absolute; + top: calc(100% - 5px); + width: calc(100% - 80px); + flex-direction: column; + border: 1px solid var(--color-mist); + border-radius: 6px; + background: #fff; + box-shadow: 0px 30px 35px var(--color-smoke-75); + min-width: 200px; + } + + #site-header .menu-item { + flex-direction: column; + } + #site-header .menu-item + .menu-item { + border-top: 1px solid var(--color-mist); + } + + #site-header .menu-item button, + #site-header .menu-item a { + padding: 10px 20px; + } + + #site-header .menu-item.has-children.active .menu-dropdown { + border-top: 1px solid var(--color-mist); + } + + #site-header .menu-dropdown-item a { + padding: 10px 20px 10px 30px; + } +} + +@media (max-width: 499px) { + #site-header nav { + width: calc(100% - 40px); + } +} \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..e22804f --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,245 @@ +# Examples + +This page demonstrates common usage patterns for Testcontainers for Zig, from basic container management through to multi-container setups. + +## Basic HTTP container + +Start an NGINX container, wait for it to be ready, and make an HTTP request: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "nginx container" { + const allocator = std.testing.allocator; + + const ctr = try tc.run(allocator, "nginx:1.26-alpine", .{ + .exposed_ports = &.{"80/tcp"}, + .wait_strategy = tc.wait.forHttp("/"), + }); + defer { + ctr.terminate() catch {}; + ctr.deinit(); + tc.deinitProvider(); + } + + const port = try ctr.mappedPort("80/tcp", allocator); + const host = try ctr.daemonHost(allocator); + defer allocator.free(host); + + const url = try std.fmt.allocPrint(allocator, "http://{s}:{d}/", .{ host, port }); + defer allocator.free(url); + + var http_client: std.http.Client = .{ .allocator = allocator }; + defer http_client.deinit(); + + const fetch_result = try http_client.fetch(.{ + .location = .{ .url = url }, + }); + std.debug.print("Status: {d}\n", .{@intFromEnum(fetch_result.status)}); +} +``` + +## Database module + +Use the pre-configured [PostgreSQL module](../modules/postgres.md) for zero-config database testing: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "postgres module" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const pg = try tc.modules.postgres.run(&provider, tc.modules.postgres.default_image, .{ + .database = "myapp_test", + .username = "admin", + .password = "secret", + }); + defer pg.terminate() catch {}; + defer pg.deinit(); + + const conn = try pg.connectionString(allocator); + defer allocator.free(conn); + // "postgres://admin:secret@localhost:PORT/myapp_test" +} +``` + +## Combined wait strategies + +Wait for multiple conditions before considering a container ready: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "combined wait strategies" { + const allocator = std.testing.allocator; + + const strategies = [_]tc.wait.Strategy{ + tc.wait.forPort("5432/tcp"), + tc.wait.forLog("database system is ready to accept connections"), + }; + + const ctr = try tc.run(allocator, "postgres:16", .{ + .env = &.{"POSTGRES_PASSWORD=password"}, + .exposed_ports = &.{"5432/tcp"}, + .wait_strategy = tc.wait.forAll(&strategies), + }); + defer { + ctr.terminate() catch {}; + ctr.deinit(); + tc.deinitProvider(); + } + + // Container is guaranteed to have port 5432 listening + // AND the log message present +} +``` + +## Multi-container network + +Connect multiple containers through a custom Docker network: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "multi-container network" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + // Create a shared network + const net = try tc.network.newNetwork(allocator, &provider.client, .{ + .name = "app-network", + }); + defer { + net.remove() catch {}; + net.deinit(); + } + + // Start PostgreSQL + const pg = try tc.modules.postgres.run(&provider, tc.modules.postgres.default_image, .{ + .database = "mydb", + .username = "admin", + .password = "secret", + }); + defer pg.terminate() catch {}; + defer pg.deinit(); + + // Start Redis + const redis = try tc.modules.redis.runDefault(&provider); + defer redis.terminate() catch {}; + defer redis.deinit(); + + const pg_conn = try pg.connectionString(allocator); + defer allocator.free(pg_conn); + const redis_conn = try redis.connectionString(allocator); + defer allocator.free(redis_conn); + + std.debug.print("PostgreSQL: {s}\n", .{pg_conn}); + std.debug.print("Redis: {s}\n", .{redis_conn}); +} +``` + +## Executing commands in a container + +Run commands inside a running container: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "exec in container" { + const allocator = std.testing.allocator; + + const ctr = try tc.run(allocator, "alpine:latest", .{ + .cmd = &.{ "sleep", "30" }, + }); + defer { + ctr.terminate() catch {}; + ctr.deinit(); + tc.deinitProvider(); + } + + const result = try ctr.exec(&.{ "echo", "Hello from Alpine" }); + defer allocator.free(result.output); + std.debug.print("{s}\n", .{std.mem.trim(u8, result.output, "\n\r ")}); +} +``` + +## Reading container logs + +Access stdout/stderr from a running container: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "container logs" { + const allocator = std.testing.allocator; + + const ctr = try tc.run(allocator, "alpine:latest", .{ + .cmd = &.{ "sh", "-c", "echo 'Application started' && sleep 30" }, + .wait_strategy = tc.wait.forLog("Application started"), + }); + defer { + ctr.terminate() catch {}; + ctr.deinit(); + tc.deinitProvider(); + } + + const logs = try ctr.logs(allocator); + defer allocator.free(logs); + std.debug.print("{s}\n", .{logs}); +} +``` + +## Zig test integration + +Testcontainers integrates naturally with Zig's built-in test framework. Use `test` blocks and `defer` for automatic cleanup: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "database reachable" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const pg = try tc.modules.postgres.run(&provider, tc.modules.postgres.default_image, .{ + .database = "testdb", + .username = "admin", + .password = "secret", + }); + defer pg.terminate() catch {}; + defer pg.deinit(); + + const conn = try pg.connectionString(allocator); + defer allocator.free(conn); + + // Verify the connection string contains our database name + try std.testing.expect(std.mem.indexOf(u8, conn, "testdb") != null); +} + +test "redis reachable" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const redis = try tc.modules.redis.runDefault(&provider); + defer redis.terminate() catch {}; + defer redis.deinit(); + + const port = try redis.port(allocator); + try std.testing.expect(port > 0); +} +``` diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..311a0acaa373f197360673f8ac6bc1b3c38c7e7f GIT binary patch literal 15406 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 + Not using Zig? Here are other supported languages! +

    + + +## About + +Testcontainers for Zig is a library to support tests with throwaway instances of Docker containers. Built with Zig 0.15.2, it communicates with Docker via the Docker Remote API over Unix domain sockets using a built-in HTTP/1.1 client — no external dependencies required. + +Choose from existing pre-configured [modules](modules/index.md) — PostgreSQL, MySQL, Redis, MongoDB, RabbitMQ, MariaDB, MinIO, Elasticsearch, Kafka, and LocalStack — and start containers within seconds. Or use the generic `ContainerRequest` struct to run any Docker image with full control over configuration. + +Read the [Quickstart](quickstart/index.md) to get up and running in minutes. + +## System requirements + +Please read the [System Requirements](system_requirements/index.md) page before you start. + +| Requirement | Minimum version | +|-----------------|----------------------| +| Zig | 0.15.2 | +| macOS | 13.0 (Ventura) | +| Linux | Ubuntu 22.04+ | +| Docker | 20.10+ | + +Testcontainers automatically detects the Docker socket. It checks the `DOCKER_HOST` environment variable first, then falls back to `/var/run/docker.sock`. + +## License + +See [LICENSE](https://github.com/dragosv/testcontainers-zig/blob/main/LICENSE). + +## Copyright + +Copyright (c) 2024 - 2026 The Testcontainers for Zig Authors. + +---- + +Join our [Slack workspace](https://slack.testcontainers.org/) | [Testcontainers OSS](https://www.testcontainers.org/) | [Testcontainers Cloud](https://testcontainers.com/cloud/) +[testcontainers-cloud]: https://www.testcontainers.cloud/ diff --git a/docs/js/tc-header.js b/docs/js/tc-header.js new file mode 100644 index 0000000..4186b6c --- /dev/null +++ b/docs/js/tc-header.js @@ -0,0 +1,45 @@ +const mobileToggle = document.getElementById("mobile-menu-toggle"); +const mobileSubToggle = document.getElementById("mobile-submenu-toggle"); +function toggleMobileMenu() { + document.body.classList.toggle('mobile-menu'); + document.body.classList.toggle("mobile-tc-header-active"); +} +function toggleMobileSubmenu() { + document.body.classList.toggle('mobile-submenu'); +} +if (mobileToggle) + mobileToggle.addEventListener("click", toggleMobileMenu); +if (mobileSubToggle) + mobileSubToggle.addEventListener("click", toggleMobileSubmenu); + +const allParentMenuItems = document.querySelectorAll("#site-header .menu-item.has-children"); +function clearActiveMenuItem() { + document.body.classList.remove("tc-header-active"); + allParentMenuItems.forEach((item) => { + item.classList.remove("active"); + }); +} +function setActiveMenuItem(e) { + clearActiveMenuItem(); + e.currentTarget.closest(".menu-item").classList.add("active"); + document.body.classList.add("tc-header-active"); +} +allParentMenuItems.forEach((item) => { + const trigger = item.querySelector(":scope > a, :scope > button"); + + trigger.addEventListener("click", (e) => { + if (e.currentTarget.closest(".menu-item").classList.contains("active")) { + clearActiveMenuItem(); + } else { + setActiveMenuItem(e); + } + }); + + trigger.addEventListener("mouseenter", (e) => { + setActiveMenuItem(e); + }); + + item.addEventListener("mouseleave", (e) => { + clearActiveMenuItem(); + }); +}); \ No newline at end of file diff --git a/docs/language-logos/dotnet.svg b/docs/language-logos/dotnet.svg new file mode 100644 index 0000000..2fb163d --- /dev/null +++ b/docs/language-logos/dotnet.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/language-logos/go.svg b/docs/language-logos/go.svg new file mode 100644 index 0000000..bfcca48 --- /dev/null +++ b/docs/language-logos/go.svg @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/docs/language-logos/haskell.svg b/docs/language-logos/haskell.svg new file mode 100644 index 0000000..eb6de37 --- /dev/null +++ b/docs/language-logos/haskell.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/language-logos/java.svg b/docs/language-logos/java.svg new file mode 100644 index 0000000..590da12 --- /dev/null +++ b/docs/language-logos/java.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/docs/language-logos/nodejs.svg b/docs/language-logos/nodejs.svg new file mode 100644 index 0000000..08c6ea7 --- /dev/null +++ b/docs/language-logos/nodejs.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/docs/language-logos/python.svg b/docs/language-logos/python.svg new file mode 100644 index 0000000..d06a313 --- /dev/null +++ b/docs/language-logos/python.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/docs/language-logos/ruby.svg b/docs/language-logos/ruby.svg new file mode 100644 index 0000000..05537ce --- /dev/null +++ b/docs/language-logos/ruby.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/language-logos/rust.svg b/docs/language-logos/rust.svg new file mode 100644 index 0000000..8903933 --- /dev/null +++ b/docs/language-logos/rust.svg @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9001e9e23ff7d9c8af5547004ecbd0aab0ad382f GIT binary patch literal 21628 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/index.md b/docs/modules/index.md new file mode 100644 index 0000000..04fd1d6 --- /dev/null +++ b/docs/modules/index.md @@ -0,0 +1,79 @@ +# Modules + +Testcontainers for Zig modules are pre-configured container wrappers that provide sensible defaults, correct wait strategies, and convenience methods for the most popular Docker images. + +Each module exposes: + +- `default_image` — the recommended image tag +- `Options` — configuration struct with sensible defaults +- `run(provider, image, opts)` — start and return a typed container +- `runDefault(provider)` — shorthand with default image and options + +## Available modules + +| Module | Default Image | Connection Helper | +|-----------------|---------------------------------------------------------------|-------------------------| +| PostgreSQL | `postgres:16-alpine` | `connectionString()` | +| MySQL | `mysql:8.0` | `connectionString()` | +| MariaDB | `mariadb:11` | `connectionString()` | +| Redis | `redis:7-alpine` | `connectionString()` | +| MongoDB | `mongo:7` | `connectionString()` | +| RabbitMQ | `rabbitmq:3-management-alpine` | `amqpURL()`, `httpURL()`| +| MinIO | `minio/minio:RELEASE.2024-01-16T16-07-38Z` | `connectionString()` | +| Elasticsearch | `docker.elastic.co/elasticsearch/elasticsearch:8.12.0` | `httpURL()` | +| Kafka | `bitnami/kafka:3.7` | `brokers()` | +| LocalStack | `localstack/localstack:3` | `endpointURL()` | + +## Usage pattern + +All modules follow the same pattern: + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "module usage" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + // Using runDefault (default image + default options) + const pg = try tc.modules.postgres.runDefault(&provider); + defer pg.terminate() catch {}; + defer pg.deinit(); + + const conn = try pg.connectionString(allocator); + defer allocator.free(conn); +} +``` + +### With custom options + +```zig +const pg = try tc.modules.postgres.run(&provider, tc.modules.postgres.default_image, .{ + .username = "admin", + .password = "secret", + .database = "mydb", +}); +defer pg.terminate() catch {}; +defer pg.deinit(); +``` + +### With a custom image + +```zig +const pg = try tc.modules.postgres.run(&provider, "postgres:15-alpine", .{}); +defer pg.terminate() catch {}; +defer pg.deinit(); +``` + +## Creating a new module + +See [AGENTS.md](https://github.com/dragosv/testcontainers-zig/blob/main/AGENTS.md) for the step-by-step guide on adding a new module. Each module should: + +1. Create `src/modules/.zig` following the existing pattern. +2. Export it in `src/root.zig`. +3. Pre-configure: `default_image`, `Options` struct with defaults, and a wait strategy. +4. Expose a connection helper returning a caller-owned `[]u8`. +5. Add tests and documentation. diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md new file mode 100644 index 0000000..8eb0e6c --- /dev/null +++ b/docs/modules/mongodb.md @@ -0,0 +1,57 @@ +# MongoDB + +The MongoDB module provides a pre-configured container for [MongoDB](https://www.mongodb.com/). + +## Usage + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "mongodb" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const mongo = try tc.modules.mongodb.runDefault(&provider); + defer mongo.terminate() catch {}; + defer mongo.deinit(); + + const conn = try mongo.connectionString(allocator); + defer allocator.free(conn); + // "mongodb://localhost:PORT" +} +``` + +## Default image + +`mongo:7` + +## Options + +| Option | Type | Default | Description | +|------------|---------------|----------|------------------------------------------| +| `username` | `[]const u8` | `""` | Admin username (empty = no auth). | +| `password` | `[]const u8` | `""` | Admin password (empty = no auth). | + +## Container methods + +| Method | Returns | Description | +|----------------------|-------------|----------------------------------------------| +| `connectionString()` | `[]const u8`| URL: `mongodb://localhost:PORT` | +| `port()` | `u16` | Mapped host port for MongoDB (27017/tcp). | +| `terminate()` | `!void` | Stop and remove the container. | +| `deinit()` | `void` | Free Zig-side memory. | + +## Wait strategy + +The module uses `wait.forLog("Waiting for connections")`. + +## Shorthand + +```zig +const mongo = try tc.modules.mongodb.runDefault(&provider); +defer mongo.terminate() catch {}; +defer mongo.deinit(); +``` diff --git a/docs/modules/mysql.md b/docs/modules/mysql.md new file mode 100644 index 0000000..9f15ab7 --- /dev/null +++ b/docs/modules/mysql.md @@ -0,0 +1,63 @@ +# MySQL + +The MySQL module provides a pre-configured container for [MySQL](https://www.mysql.com/). + +## Usage + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "mysql" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const mysql = try tc.modules.mysql.run(&provider, tc.modules.mysql.default_image, .{ + .username = "admin", + .password = "secret", + .database = "testdb", + }); + defer mysql.terminate() catch {}; + defer mysql.deinit(); + + const conn = try mysql.connectionString(allocator); + defer allocator.free(conn); + // "mysql://admin:secret@localhost:PORT/testdb" +} +``` + +## Default image + +`mysql:8.0` + +## Options + +| Option | Type | Default | Description | +|-----------------|---------------|------------|-------------------------| +| `username` | `[]const u8` | `"test"` | Database user. | +| `password` | `[]const u8` | `"test"` | User password. | +| `root_password` | `[]const u8` | `"root"` | Root password. | +| `database` | `[]const u8` | `"test"` | Database name. | + +## Container methods + +| Method | Returns | Description | +|----------------------|-------------|-----------------------------------------------------| +| `connectionString()` | `[]const u8`| URL: `mysql://user:pass@host:port/db` | +| `port()` | `u16` | Mapped host port for MySQL (3306/tcp). | +| `terminate()` | `!void` | Stop and remove the container. | +| `deinit()` | `void` | Free Zig-side memory. | + +## Wait strategy + +The module uses `wait.forLog("port: 3306 MySQL Community Server")`. + +## Shorthand + +```zig +const mysql = try tc.modules.mysql.runDefault(&provider); +defer mysql.terminate() catch {}; +defer mysql.deinit(); +``` diff --git a/docs/modules/postgres.md b/docs/modules/postgres.md new file mode 100644 index 0000000..53ac6cc --- /dev/null +++ b/docs/modules/postgres.md @@ -0,0 +1,66 @@ +# PostgreSQL + +The PostgreSQL module provides a pre-configured container for [PostgreSQL](https://www.postgresql.org/). + +## Usage + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "postgres" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const pg = try tc.modules.postgres.run(&provider, tc.modules.postgres.default_image, .{ + .username = "admin", + .password = "secret", + .database = "testdb", + }); + defer pg.terminate() catch {}; + defer pg.deinit(); + + const conn = try pg.connectionString(allocator); + defer allocator.free(conn); + // "postgres://admin:secret@localhost:PORT/testdb" + + const port = try pg.port(allocator); + std.debug.print("PostgreSQL on port {d}\n", .{port}); +} +``` + +## Default image + +`postgres:16-alpine` + +## Options + +| Option | Type | Default | Description | +|------------|---------------|--------------|-------------------------| +| `username` | `[]const u8` | `"postgres"` | Database user. | +| `password` | `[]const u8` | `"postgres"` | User password. | +| `database` | `[]const u8` | `"postgres"` | Database name. | + +## Container methods + +| Method | Returns | Description | +|----------------------|-------------|----------------------------------------------------------| +| `connectionString()` | `[]const u8`| libpq-compatible URL: `postgres://user:pass@host:port/db`| +| `port()` | `u16` | Mapped host port for PostgreSQL (5432/tcp). | +| `terminate()` | `!void` | Stop and remove the container. | +| `deinit()` | `void` | Free Zig-side memory. | + +## Wait strategy + +The module uses `wait.forLog("database system is ready to accept connections")`. + +## Shorthand + +```zig +// Default image + default options +const pg = try tc.modules.postgres.runDefault(&provider); +defer pg.terminate() catch {}; +defer pg.deinit(); +``` diff --git a/docs/modules/redis.md b/docs/modules/redis.md new file mode 100644 index 0000000..6619969 --- /dev/null +++ b/docs/modules/redis.md @@ -0,0 +1,60 @@ +# Redis + +The Redis module provides a pre-configured container for [Redis](https://redis.io/). + +## Usage + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "redis" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const redis = try tc.modules.redis.runDefault(&provider); + defer redis.terminate() catch {}; + defer redis.deinit(); + + const conn = try redis.connectionString(allocator); + defer allocator.free(conn); + // "redis://localhost:PORT" + + const port = try redis.port(allocator); + std.debug.print("Redis on port {d}\n", .{port}); +} +``` + +## Default image + +`redis:7-alpine` + +## Options + +| Option | Type | Default | Description | +|--------------|------------------------|----------|--------------------------------------| +| `password` | `[]const u8` | `""` | Redis password (empty = no auth). | +| `extra_args` | `[]const []const u8` | `&.{}` | Additional command-line arguments. | + +## Container methods + +| Method | Returns | Description | +|----------------------|-------------|----------------------------------------------| +| `connectionString()` | `[]const u8`| URL: `redis://localhost:PORT` | +| `port()` | `u16` | Mapped host port for Redis (6379/tcp). | +| `terminate()` | `!void` | Stop and remove the container. | +| `deinit()` | `void` | Free Zig-side memory. | + +## Wait strategy + +The module uses `wait.forLog("Ready to accept connections")`. + +## Shorthand + +```zig +const redis = try tc.modules.redis.runDefault(&provider); +defer redis.terminate() catch {}; +defer redis.deinit(); +``` diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md new file mode 100644 index 0000000..8dafb25 --- /dev/null +++ b/docs/quickstart/index.md @@ -0,0 +1,102 @@ +# Quickstart + +Testcontainers for Zig integrates with Zig's built-in `test` blocks and `std.testing` framework. + +It is designed for integration and end-to-end tests, helping you spin up and manage the lifecycle of container-based dependencies via Docker. + +## 1. System requirements + +Please read the [System Requirements](../system_requirements/index.md) page before you start. + +## 2. Install Testcontainers for Zig + +Add testcontainers-zig as a dependency in your `build.zig.zon`: + +```zig +.{ + .name = .my_project, + .version = "0.1.0", + .dependencies = .{ + .testcontainers = .{ + .url = "https://github.com/dragosv/testcontainers-zig/archive/refs/heads/main.tar.gz", + // Replace with the actual hash after first fetch + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} +``` + +Then add it to your `build.zig`: + +```zig +const tc_dep = b.dependency("testcontainers", .{ + .target = target, + .optimize = optimize, +}); + +// Add to your test step +const tests = b.addTest(.{ + .root_source_file = b.path("src/main_test.zig"), + .target = target, + .optimize = optimize, +}); +tests.root_module.addImport("testcontainers", tc_dep.module("testcontainers")); +``` + +## 3. Spin up Redis + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "redis container" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const redis = try tc.modules.redis.runDefault(&provider); + defer redis.terminate() catch {}; + defer redis.deinit(); + + const conn = try redis.connectionString(allocator); + defer allocator.free(conn); + + // conn = "redis://localhost:PORT" + std.debug.print("Redis available at {s}\n", .{conn}); +} +``` + +The `ContainerRequest` struct configures the container using struct-literal syntax with named fields and sensible defaults. + +- `exposed_ports` specifies which ports to publish from the container. Docker maps each to a random available host port. +- `wait_strategy` validates when a container is ready to receive traffic. For Redis, the module uses a log-based wait strategy. + +Docker maps each container port to a random available host port. This is crucial for parallelization — if you add multiple tests, each starts its own Redis container on a different random port. + +All containers must be removed at some point, otherwise they will run until the host is overloaded. Using `defer` ensures cleanup happens even if the test fails. + +!!! tip + + Look at [Garbage Collector](../features/garbage_collector.md) to learn more about resource cleanup patterns. + +## 4. Connect your code to the container + +In a real project, you would pass this endpoint to your Redis client library. This snippet retrieves the endpoint from the container we just started: + +```zig +const conn = try redis.connectionString(allocator); +defer allocator.free(conn); +// Use conn with your Redis client library +// Returns: "redis://localhost:PORT" +``` + +The connection string includes the randomly mapped host port. + +!!! tip + + If you expose more than one port, use `container.mappedPort("PORT/tcp", allocator)` with the specific container port you need. diff --git a/docs/system_requirements/ci/dind_patterns.md b/docs/system_requirements/ci/dind_patterns.md new file mode 100644 index 0000000..e7260e9 --- /dev/null +++ b/docs/system_requirements/ci/dind_patterns.md @@ -0,0 +1,52 @@ +# Docker-in-Docker (DinD) patterns + +When running Testcontainers in CI environments that don't have Docker natively available, you can use Docker-in-Docker patterns. + +## GitHub Actions + +GitHub-hosted runners include Docker, so DinD is not typically needed. See [GitHub Actions](github_actions.md). + +## GitLab CI with DinD service + +```yaml +test: + stage: test + image: debian:bookworm-slim + services: + - docker:dind + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" + before_script: + - apt-get update && apt-get install -y curl xz-utils docker.io + - curl -L https://ziglang.org/download/0.15.2/zig-linux-x86_64-0.15.2.tar.xz | tar xJ + - export PATH="$PWD/zig-linux-x86_64-0.15.2:$PATH" + script: + - zig build test --summary all + - zig build integration-test --summary all +``` + +## Generic Docker-in-Docker + +If you're running inside a Docker container and need to use Testcontainers, mount the Docker socket: + +```bash +docker run -v /var/run/docker.sock:/var/run/docker.sock \ + -v $(pwd):/workspace -w /workspace \ + my-zig-builder:latest \ + zig build integration-test --summary all +``` + +## Environment variables + +When using DinD or remote Docker hosts, set: + +```bash +export DOCKER_HOST=tcp://docker:2375 +``` + +Testcontainers for Zig respects the `DOCKER_HOST` environment variable and will connect to the specified Docker daemon instead of the default Unix socket. + +!!! warning + + When using `DOCKER_HOST` with a TCP address, ensure the Docker daemon is configured to accept unauthenticated connections or configure TLS appropriately. diff --git a/docs/system_requirements/ci/github_actions.md b/docs/system_requirements/ci/github_actions.md new file mode 100644 index 0000000..f15de31 --- /dev/null +++ b/docs/system_requirements/ci/github_actions.md @@ -0,0 +1,61 @@ +# GitHub Actions + +Testcontainers for Zig works out of the box with GitHub Actions. GitHub-hosted runners include Docker pre-installed. + +## Example workflow + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Run unit tests + run: zig build test --summary all + + - name: Run integration tests + run: zig build integration-test --summary all +``` + +## Notes + +- GitHub-hosted Ubuntu runners have Docker pre-installed — no additional setup is required. +- The `mlugg/setup-zig` action installs the specified Zig version. +- Unit tests (`zig build test`) do not require Docker. Integration tests (`zig build integration-test`) do. + +## macOS runners + +```yaml + test-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Install Docker + run: brew install --cask docker + + - name: Run unit tests + run: zig build test --summary all +``` + +!!! note + + macOS runners do not have Docker pre-installed. You need to install Docker Desktop via Homebrew or similar. diff --git a/docs/system_requirements/ci/gitlab_ci.md b/docs/system_requirements/ci/gitlab_ci.md new file mode 100644 index 0000000..f24f90d --- /dev/null +++ b/docs/system_requirements/ci/gitlab_ci.md @@ -0,0 +1,43 @@ +# GitLab CI/CD + +Testcontainers for Zig can run on GitLab CI/CD using Docker-in-Docker or a shell executor with Docker installed. + +## Docker-in-Docker + +```yaml +stages: + - test + +test: + stage: test + image: debian:bookworm-slim + services: + - docker:dind + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" + before_script: + - apt-get update && apt-get install -y curl xz-utils docker.io + - curl -L https://ziglang.org/download/0.15.2/zig-linux-x86_64-0.15.2.tar.xz | tar xJ + - export PATH="$PWD/zig-linux-x86_64-0.15.2:$PATH" + script: + - zig build test --summary all + - zig build integration-test --summary all +``` + +## Shell executor + +If your GitLab runner uses the shell executor with Docker already installed: + +```yaml +test: + stage: test + tags: + - shell + before_script: + - curl -L https://ziglang.org/download/0.15.2/zig-linux-x86_64-0.15.2.tar.xz | tar xJ + - export PATH="$PWD/zig-linux-x86_64-0.15.2:$PATH" + script: + - zig build test --summary all + - zig build integration-test --summary all +``` diff --git a/docs/system_requirements/index.md b/docs/system_requirements/index.md new file mode 100644 index 0000000..06010df --- /dev/null +++ b/docs/system_requirements/index.md @@ -0,0 +1,48 @@ +# System requirements + +Testcontainers for Zig has the following system requirements: + +| Requirement | Minimum version | +|-----------------|----------------------| +| Zig | 0.15.2 | +| macOS | 13.0 (Ventura) | +| Linux | Ubuntu 22.04+ | +| Docker | 20.10+ | + +## Zig + +Testcontainers for Zig is built with Zig 0.15.2 and uses the Zig Build System (`build.zig` + `build.zig.zon`). No external dependencies are required. + +## Docker + +A Docker-compatible container runtime is required: + +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (macOS, Windows, Linux) +- [Docker Engine](https://docs.docker.com/engine/) (Linux) +- [Rancher Desktop](https://rancherdesktop.io/) (macOS, Windows, Linux) +- [Podman](https://podman.io/) with Docker-compatible socket + +The library communicates with Docker over its Unix domain socket (`/var/run/docker.sock`). Set the `DOCKER_HOST` environment variable if your Docker socket is at a non-standard location. + +## Checking your setup + +Verify Docker is available: + +```bash +docker info +``` + +Run the tests: + +```bash +zig build test --summary all # Unit tests (no Docker required) +zig build integration-test --summary all # Integration tests (requires Docker) +``` + +## CI/CD + +Testcontainers for Zig works in CI environments that provide Docker access. See: + +- [GitHub Actions](ci/github_actions.md) +- [GitLab CI/CD](ci/gitlab_ci.md) +- [Docker-in-Docker patterns](ci/dind_patterns.md) diff --git a/docs/test_frameworks/zig_test.md b/docs/test_frameworks/zig_test.md new file mode 100644 index 0000000..e571c7c --- /dev/null +++ b/docs/test_frameworks/zig_test.md @@ -0,0 +1,120 @@ +# Zig Test Integration + +Testcontainers for Zig integrates with Zig's built-in test framework. Tests are written as `test` blocks using `std.testing` assertions. + +## Basic test pattern + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "container in test" { + const allocator = std.testing.allocator; + + const ctr = try tc.run(allocator, "nginx:latest", .{ + .exposed_ports = &.{"80/tcp"}, + .wait_strategy = tc.wait.forHttp("/"), + }); + defer { + ctr.terminate() catch {}; + ctr.deinit(); + tc.deinitProvider(); + } + + const port = try ctr.mappedPort("80/tcp", allocator); + try std.testing.expect(port > 0); +} +``` + +## Using modules in tests + +```zig +const std = @import("std"); +const tc = @import("testcontainers"); + +test "postgres in test" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const pg = try tc.modules.postgres.run(&provider, tc.modules.postgres.default_image, .{ + .database = "testdb", + .username = "admin", + .password = "secret", + }); + defer pg.terminate() catch {}; + defer pg.deinit(); + + const conn = try pg.connectionString(allocator); + defer allocator.free(conn); + + try std.testing.expect(std.mem.indexOf(u8, conn, "testdb") != null); +} +``` + +## Skipping tests when Docker is unavailable + +Integration tests that require Docker should check for availability and skip gracefully: + +```zig +test "requires docker" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + // Try to connect to Docker — skip if unavailable + _ = provider.client.imageExists("alpine:latest") catch { + return error.SkipZigTest; + }; + + // ... rest of test +} +``` + +## Multiple containers in one test + +```zig +test "multi-container test" { + const allocator = std.testing.allocator; + + var provider = tc.DockerProvider.init(allocator); + defer provider.deinit(); + + const pg = try tc.modules.postgres.runDefault(&provider); + defer pg.terminate() catch {}; + defer pg.deinit(); + + const redis = try tc.modules.redis.runDefault(&provider); + defer redis.terminate() catch {}; + defer redis.deinit(); + + const pg_conn = try pg.connectionString(allocator); + defer allocator.free(pg_conn); + + const redis_conn = try redis.connectionString(allocator); + defer allocator.free(redis_conn); + + try std.testing.expect(std.mem.indexOf(u8, pg_conn, "postgres") != null); + try std.testing.expect(std.mem.indexOf(u8, redis_conn, "redis") != null); +} +``` + +## Running tests + +```bash +# Run unit tests only (no Docker required) +zig build test --summary all + +# Run integration tests (requires Docker) +zig build integration-test --summary all +``` + +## Best practices + +- Create a fresh `DockerProvider` per test function — do not share providers across tests. +- Always use `defer` for cleanup: `defer ctr.terminate() catch {};` and `defer ctr.deinit();`. +- Use `std.testing.allocator` which detects memory leaks. +- Use `error.SkipZigTest` when Docker is not available to allow graceful test skipping. +- Pin image versions (e.g. `postgres:16-alpine`) to ensure reproducible tests. diff --git a/docs/testcontainers-logo.svg b/docs/testcontainers-logo.svg new file mode 100644 index 0000000..4b099f3 --- /dev/null +++ b/docs/testcontainers-logo.svg @@ -0,0 +1,22 @@ + + + Testcontainers + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/theme/main.html b/docs/theme/main.html new file mode 100644 index 0000000..b3c01a3 --- /dev/null +++ b/docs/theme/main.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block analytics %} + +{% endblock %} + +{% block extrahead %} + + +{% endblock %} \ No newline at end of file diff --git a/docs/theme/partials/header.html b/docs/theme/partials/header.html new file mode 100644 index 0000000..057ec4a --- /dev/null +++ b/docs/theme/partials/header.html @@ -0,0 +1,150 @@ + + + +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} + +{% include "partials/tc-header.html" %} + + +
    + + + + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
    \ No newline at end of file diff --git a/docs/theme/partials/nav.html b/docs/theme/partials/nav.html new file mode 100644 index 0000000..87a254e --- /dev/null +++ b/docs/theme/partials/nav.html @@ -0,0 +1,79 @@ + + + +{% set class = "md-nav md-nav--primary" %} +{% if "navigation.tabs" in features %} +{% set class = class ~ " md-nav--lifted" %} +{% endif %} +{% if "toc.integrate" in features %} +{% set class = class ~ " md-nav--integrated" %} +{% endif %} + + + \ No newline at end of file diff --git a/docs/theme/partials/tc-header.html b/docs/theme/partials/tc-header.html new file mode 100644 index 0000000..73500c6 --- /dev/null +++ b/docs/theme/partials/tc-header.html @@ -0,0 +1,157 @@ +{% set header = ({ + "siteUrl": "https://testcontainers.com/", + "menuItems": [ + { + "label": "Desktop", + "url": "https://testcontainers.com/desktop/" + }, + { + "label": "Cloud", + "url": "https://testcontainers.com/cloud/" + }, + { + "label": "Getting Started", + "url": "https://testcontainers.com/getting-started/" + }, + { + "label": "Guides", + "url": "https://testcontainers.com/guides/" + }, + { + "label": "Modules", + "url": "https://testcontainers.com/modules/" + }, + { + "label": "Docs", + "children": [ + { + "label": "Testcontainers for Java", + "url": "https://java.testcontainers.org/", + "image": "/language-logos/java.svg", + }, + { + "label": "Testcontainers for Go", + "url": "https://golang.testcontainers.org/", + "image": "/language-logos/go.svg", + }, + { + "label": "Testcontainers for .NET", + "url": "https://dotnet.testcontainers.org/", + "image": "/language-logos/dotnet.svg", + }, + { + "label": "Testcontainers for Node.js", + "url": "https://node.testcontainers.org/", + "image": "/language-logos/nodejs.svg", + }, + { + "label": "Testcontainers for Python", + "url": "https://testcontainers-python.readthedocs.io/en/latest/", + "image": "/language-logos/python.svg", + "external": true, + }, + { + "label": "Testcontainers for Rust", + "url": "https://docs.rs/testcontainers/latest/testcontainers/", + "image": "/language-logos/rust.svg", + "external": true, + }, + { + "label": "Testcontainers for Haskell", + "url": "https://github.com/testcontainers/testcontainers-hs", + "image": "/language-logos/haskell.svg", + "external": true, + }, + { + "label": "Testcontainers for Ruby", + "url": "https://github.com/testcontainers/testcontainers-ruby", + "image": "/language-logos/ruby.svg", + "external": true, + }, + ] + }, + { + "label": "Slack", + "url": "https://slack.testcontainers.org/", + "icon": "icon-slack", + }, + { + "label": "GitHub", + "url": "https://github.com/testcontainers", + "icon": "icon-github", + }, + ] +}) %} + + + + + + + + + + + \ No newline at end of file