tstest/integration: run Windows service test via the normal windows CI run

Updates #20381

Signed-off-by: Yaruk Asghar <yaruk@tailscale.com>
This commit is contained in:
Yaruk Asghar
2026-07-10 13:37:48 -07:00
parent 820cb98dc9
commit fd4680c02c
4 changed files with 64 additions and 35 deletions

View File

@@ -238,8 +238,6 @@ jobs:
shard: "1/2"
- key: "win-shard-2-2"
shard: "2/2"
- key: "win-service"
name: "service"
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -264,7 +262,7 @@ jobs:
- name: test
shell: bash
if: startsWith(matrix.key, 'win-shard-') # sharded testwrapper legs only
if: matrix.key != 'win-bench' # skip on bench builder
working-directory: src
run: ./tool/go run ./cmd/testwrapper sharded:${{ matrix.shard }}
env:
@@ -278,14 +276,6 @@ jobs:
env:
NOPWSHDEBUG: "true" # to quiet tool/gocross/gocross-wrapper.ps1 in CI
- name: windows service integration test
shell: bash
if: matrix.key == 'win-service' # tailscaled as a real service, outside testwrapper's retry clamp
working-directory: src
run: ./tool/go test ./tstest/integration -run '^TestOneNodeUpWindowsStyle$' -run-windows-service-tests -timeout 20m -count=1 -v
env:
NOPWSHDEBUG: "true" # to quiet tool/gocross/gocross-wrapper.ps1 in CI
- name: Print stats
shell: pwsh
if: steps.cigocacher-setup.outputs.success == 'true'

View File

@@ -50,6 +50,7 @@
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/nettype"
"tailscale.com/util/cibuild"
"tailscale.com/util/rands"
"tailscale.com/util/zstdframe"
"tailscale.com/version"
@@ -59,9 +60,10 @@
verboseTailscaled = flag.Bool("verbose-tailscaled", false, "verbose tailscaled logging")
verboseTailscale = flag.Bool("verbose-tailscale", false, "verbose tailscale CLI logging")
// runWindowsServiceTests opts Windows tests into starting tailscaled as a
// service instead of a userspace child process. Tests run serially in this mode.
runWindowsServiceTests = flag.Bool("run-windows-service-tests", false, "run tailscaled as a Windows service")
// runWindowsServiceTests enables the Windows service-mode integration tests
// (tailscaled installed as a service). On by default in CI. Tests opt in via
// NewTestEnv(t, WindowsServiceMode()) and run serially in this mode.
runWindowsServiceTests = flag.Bool("run-windows-service-tests", cibuild.On(), "run Windows service-mode integration tests")
)
// MainError is an error that's set if an error conditions happens outside of a
@@ -540,11 +542,36 @@ func (f ConfigureControl) ModifyTestEnv(te *TestEnv) {
f(te.Control)
}
// windowsServiceOpt is the TestEnvOpt returned by WindowsServiceMode.
type windowsServiceOpt struct{}
func (windowsServiceOpt) ModifyTestEnv(te *TestEnv) { te.windowsService = true }
// WindowsServiceMode makes NewTestEnv run tailscaled as a Windows service
// (install-system-daemon) rather than a userspace child process. It's a no-op
// off Windows. Only tests that pass it run on Windows; all others skip there.
func WindowsServiceMode() TestEnvOpt { return windowsServiceOpt{} }
// NewTestEnv starts a bunch of services and returns a new test environment.
// NewTestEnv arranges for the environment's resources to be cleaned up on exit.
func NewTestEnv(t testing.TB, opts ...TestEnvOpt) *TestEnv {
if runtime.GOOS == "windows" && !*runWindowsServiceTests {
t.Skip("Windows only runs with --run-windows-service-tests (tailscaled as a service)")
// Integration tests skip on Windows unless a test opts into service mode
// via WindowsServiceMode(); a Windows service is a singleton, so the generic
// (often multi-node) tests can't run against it. Pre-scan the opts for the
// service marker before starting any servers so a skip leaks nothing.
serviceMode := false
for _, o := range opts {
if _, ok := o.(windowsServiceOpt); ok {
serviceMode = true
}
}
if runtime.GOOS == "windows" {
if !serviceMode {
t.Skip("integration tests skip on Windows unless run in service mode")
}
if !*runWindowsServiceTests {
t.Skip("Windows service tests disabled (--run-windows-service-tests=false)")
}
}
derpMap := RunDERPAndSTUN(t, logger.Discard, "127.0.0.1")
logc := new(LogCatcher)
@@ -557,7 +584,6 @@ func NewTestEnv(t testing.TB, opts ...TestEnvOpt) *TestEnv {
binaries := GetBinaries(t)
e := &TestEnv{
t: t,
windowsService: runtime.GOOS == "windows" && *runWindowsServiceTests,
cli: binaries.Tailscale.Path,
daemon: binaries.Tailscaled.Path,
LogCatcher: logc,

View File

@@ -1181,24 +1181,6 @@ func TestNoControlConnWhenDown(t *testing.T) {
d2.MustCleanShutdown(t)
}
// Issue 2137: make sure Windows tailscaled works with the CLI alone,
// without the GUI to kick off a Start.
func TestOneNodeUpWindowsStyle(t *testing.T) {
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS("windows")
n1.AwaitResponding()
n1.MustUp("--unattended")
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
// TestClientSideJailing tests that when one node is jailed for another, the
// jailed node cannot initiate connections to the other node however the other
// node can initiate connections to the jailed node.

View File

@@ -0,0 +1,31 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows
package integration
import (
"testing"
"tailscale.com/tstest"
)
// Issue 2137: make sure Windows tailscaled works with the CLI alone, without
// the GUI to kick off a Start. Runs tailscaled as a real Windows service via
// WindowsServiceMode; see NewTestEnv's Windows gating.
func TestOneNodeUpWindowsStyle(t *testing.T) {
tstest.Parallel(t)
env := NewTestEnv(t, WindowsServiceMode())
n1 := NewTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS("windows")
n1.AwaitResponding()
n1.MustUp("--unattended")
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}