Skip to content

Commit 257269b

Browse files
committed
CI: Run Docker SMB Rust integration tests in CI and check.sh
- New `desktop-rust-integration-tests` check in `scripts/check/` starts the core SMB Docker containers via `apps/desktop/test/smb-servers/start.sh`, waits for them to reach `running`, runs `cargo nextest run --release --run-ignored only -E 'test(smb_integration_)'` in `apps/desktop/src-tauri`, and tears the containers down (defer) — pass or fail. - Registered in the fast tier (same bucket as `desktop-rust-tests`), `FreestyleIncompat: true` since it needs Docker. - Wired into `.github/workflows/ci.yml` as a dedicated step after the existing `desktop-rust-tests` step in the `desktop-rust` job. - Docs: one-liner in `apps/desktop/test/smb-servers/README.md` and a table row in `scripts/check/CLAUDE.md`. - Closes the gap that let the compound-write cancel/progress bug (`f948731c`) sit on main undetected for four days — all 26 `smb_integration_*` tests were `#[ignore]`-gated and nothing in CI ran them.
1 parent 97062e6 commit 257269b

5 files changed

Lines changed: 163 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ jobs:
111111
- name: Run Rust tests
112112
run: ./scripts/check/check --check desktop-rust-tests --ci
113113

114+
- name: Run SMB integration tests
115+
run: ./scripts/check/check --check desktop-rust-integration-tests --ci
116+
114117
# ===========================================
115118
# Desktop app - Svelte frontend
116119
# ===========================================

apps/desktop/test/smb-servers/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ Docker SMB containers for local development and E2E testing, provided by smb2's
1414
The Docker Compose files live in `.compose/`. They're **vendored** from smb2's consumer test harness (see
1515
`.compose/VENDORED.md`) — if they're missing or stale after an smb2 bump, follow the re-vendor steps there.
1616

17+
CI runs the Rust SMB integration tests automatically via the `desktop-rust-integration-tests` check, which starts the
18+
`core` containers, runs `cargo nextest run --run-ignored only -E 'test(smb_integration_)'`, and tears them down.
19+
Locally, `./scripts/check.sh --rust` includes the same check.
20+
1721
See [docs/guides/testing/smb-servers.md](../../../../docs/guides/testing/smb-servers.md) for the full documentation.

scripts/check/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ before tests.
187187

188188
| App | Tech | Checks |
189189
| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190-
| Desktop | Rust | rustfmt, clippy, cargo-audit, cargo-deny, cargo-udeps, jscpd, tests, tests-linux (slow) |
190+
| Desktop | Rust | rustfmt, clippy, cargo-audit, cargo-deny, cargo-udeps, jscpd, tests, integration-tests (Docker SMB), tests-linux (slow) |
191191
| Desktop | Svelte | prettier, eslint, eslint-typecheck (slow), stylelint, css-unused, a11y-contrast, svelte-check, import-cycles, knip, type-drift, tests, e2e-linux-typecheck, e2e-linux (slow), e2e-playwright (slow) |
192192
| Website | Astro | prettier, eslint, typecheck, build, html-validate, e2e |
193193
| Website | Docker | docker-build |
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package checks
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"path/filepath"
7+
"regexp"
8+
"strconv"
9+
"strings"
10+
"time"
11+
)
12+
13+
// RunRustIntegrationTests runs the Docker-backed SMB Rust integration tests.
14+
//
15+
// Flow:
16+
// 1. Start the core SMB Docker containers (guest, auth, both, readonly, flaky, slow)
17+
// via apps/desktop/test/smb-servers/start.sh.
18+
// 2. Poll until the expected containers report `running`.
19+
// 3. Invoke `cargo nextest run --release --run-ignored only -E 'test(smb_integration_)'`
20+
// in apps/desktop/src-tauri. The expression filter matches every
21+
// `smb_integration_*` test and skips other `#[ignore]` tests.
22+
// 4. Always tear the containers down afterwards (success or failure).
23+
func RunRustIntegrationTests(ctx *CheckContext) (CheckResult, error) {
24+
// Docker is a hard requirement. Surface a clear message instead of a cryptic error.
25+
if !CommandExists("docker") {
26+
return CheckResult{}, fmt.Errorf(
27+
"docker is required for SMB integration tests — install Docker or run without this check",
28+
)
29+
}
30+
if _, err := RunCommand(exec.Command("docker", "info"), true); err != nil {
31+
return CheckResult{}, fmt.Errorf(
32+
"docker daemon is not running — start Docker or run without this check",
33+
)
34+
}
35+
36+
smbServersDir := filepath.Join(ctx.RootDir, "apps", "desktop", "test", "smb-servers")
37+
rustDir := filepath.Join(ctx.RootDir, "apps", "desktop", "src-tauri")
38+
39+
// Start containers (core = guest, auth, both, readonly, flaky, slow). The
40+
// make_docker_volume helper in smb.rs only uses the guest port today, but
41+
// core matches what the default start.sh spins up and covers anything new
42+
// we add.
43+
startCmd := exec.Command("./start.sh", "core")
44+
startCmd.Dir = smbServersDir
45+
if startOutput, err := RunCommand(startCmd, true); err != nil {
46+
return CheckResult{}, fmt.Errorf("couldn't start SMB containers\n%s", indentOutput(startOutput))
47+
}
48+
49+
// Always stop containers when the check returns, regardless of outcome.
50+
defer func() {
51+
stopCmd := exec.Command("./stop.sh")
52+
stopCmd.Dir = smbServersDir
53+
_, _ = RunCommand(stopCmd, true)
54+
}()
55+
56+
// Wait for the core services to be running. We don't require `healthy`
57+
// here because these images don't all ship healthchecks; `running` plus
58+
// a short settle is enough, and smb2 reconnects if the server isn't
59+
// ready on the first write.
60+
expected := []string{
61+
"smb-consumer-guest",
62+
"smb-consumer-auth",
63+
"smb-consumer-both",
64+
"smb-consumer-readonly",
65+
"smb-consumer-flaky",
66+
"smb-consumer-slow",
67+
}
68+
if err := waitForSmbContainers(expected, 120*time.Second); err != nil {
69+
return CheckResult{}, err
70+
}
71+
72+
// Make sure cargo-nextest is available (mirrors desktop-rust-tests.go).
73+
if !CommandExists("cargo-nextest") {
74+
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--locked")
75+
if _, err := RunCommand(installCmd, true); err != nil {
76+
return CheckResult{}, fmt.Errorf("failed to install cargo-nextest: %w", err)
77+
}
78+
}
79+
80+
// Use --release to match the perf profile of shipped code — compound reads
81+
// and writes are sensitive to -O settings. nextest's expression filter
82+
// matches only our `smb_integration_*` tests, so unrelated `#[ignore]`
83+
// tests are still skipped.
84+
cmd := exec.Command(
85+
"cargo", "nextest", "run",
86+
"--release",
87+
"--run-ignored", "only",
88+
"-E", "test(smb_integration_)",
89+
)
90+
cmd.Dir = rustDir
91+
output, err := RunCommand(cmd, true)
92+
if err != nil {
93+
return CheckResult{}, fmt.Errorf("SMB integration tests failed\n%s", indentOutput(output))
94+
}
95+
96+
re := regexp.MustCompile(`(\d+) tests? run`)
97+
matches := re.FindStringSubmatch(output)
98+
if len(matches) > 1 {
99+
count, _ := strconv.Atoi(matches[1])
100+
result := Success(fmt.Sprintf("%d %s passed", count, Pluralize(count, "test", "tests")))
101+
result.Total = count
102+
return result, nil
103+
}
104+
return Success("All SMB integration tests passed"), nil
105+
}
106+
107+
// waitForSmbContainers polls `docker compose -p smb-consumer ps` until every
108+
// expected service appears in the running set, or the timeout expires.
109+
func waitForSmbContainers(expected []string, timeout time.Duration) error {
110+
deadline := time.Now().Add(timeout)
111+
interval := 1 * time.Second
112+
113+
for {
114+
psCmd := exec.Command(
115+
"docker", "compose", "-p", "smb-consumer",
116+
"ps", "--services", "--filter", "status=running",
117+
)
118+
output, _ := RunCommand(psCmd, true)
119+
120+
running := make(map[string]struct{})
121+
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
122+
if line = strings.TrimSpace(line); line != "" {
123+
running[line] = struct{}{}
124+
}
125+
}
126+
127+
missing := []string{}
128+
for _, svc := range expected {
129+
if _, ok := running[svc]; !ok {
130+
missing = append(missing, svc)
131+
}
132+
}
133+
if len(missing) == 0 {
134+
return nil
135+
}
136+
137+
if time.Now().After(deadline) {
138+
return fmt.Errorf(
139+
"SMB containers didn't reach running state within %s: still waiting for %s",
140+
timeout, strings.Join(missing, ", "),
141+
)
142+
}
143+
time.Sleep(interval)
144+
}
145+
}

scripts/check/checks/registry.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ var AllChecks = []CheckDefinition{
9797
DependsOn: []string{"desktop-rust-clippy"},
9898
Run: RunRustTests,
9999
},
100+
{
101+
ID: "desktop-rust-integration-tests",
102+
Nickname: "rust-integration-tests",
103+
DisplayName: "integration tests (SMB)",
104+
App: AppDesktop,
105+
Tech: "🦀 Rust",
106+
FreestyleIncompat: true, // Needs Docker, which isn't available on freestyle.sh VMs
107+
DependsOn: []string{"desktop-rust-clippy"},
108+
Run: RunRustIntegrationTests,
109+
},
100110
{
101111
ID: "desktop-rust-tests-linux",
102112
Nickname: "rust-tests-linux",

0 commit comments

Comments
 (0)