Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34dc7fca99 | |||
| 17892264c6 | |||
| c237c867b3 | |||
| 0d23bc0d21 | |||
| d1665e49f8 | |||
| 97e12ca4c0 | |||
| a1c75df06c | |||
| 2d19519e6a | |||
| 47d9931398 |
@@ -636,7 +636,7 @@ lint/ts: site/node_modules/.installed
|
||||
lint/go:
|
||||
./scripts/check_enterprise_imports.sh
|
||||
./scripts/check_codersdk_imports.sh
|
||||
linter_ver=$(shell egrep -o 'GOLANGCI_LINT_VERSION=\S+' dogfood/coder/Dockerfile | cut -d '=' -f 2)
|
||||
linter_ver=$$(grep -oE 'GOLANGCI_LINT_VERSION=\S+' dogfood/coder/Dockerfile | cut -d '=' -f 2)
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v$$linter_ver run
|
||||
go tool github.com/coder/paralleltestctx/cmd/paralleltestctx -custom-funcs="testutil.Context" ./...
|
||||
.PHONY: lint/go
|
||||
|
||||
@@ -141,6 +141,17 @@ func TestReapInterrupt(t *testing.T) {
|
||||
}()
|
||||
|
||||
require.Equal(t, <-usrSig, syscall.SIGUSR1)
|
||||
|
||||
// Prevent SIGINT from terminating the test process. Under the
|
||||
// race detector, the catchSignals goroutine in ForkReap may not
|
||||
// have called signal.Notify yet, so the default Go handler
|
||||
// could kill us. Registering our own Notify disables the
|
||||
// default behavior. Both this channel and the one inside
|
||||
// catchSignals receive independent copies of the signal.
|
||||
intC := make(chan os.Signal, 1)
|
||||
signal.Notify(intC, os.Interrupt)
|
||||
defer signal.Stop(intC)
|
||||
|
||||
err := syscall.Kill(os.Getpid(), syscall.SIGINT)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, <-usrSig, syscall.SIGUSR2)
|
||||
|
||||
+21
-16
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/go-reap"
|
||||
@@ -19,20 +20,7 @@ func IsInitProcess() bool {
|
||||
return os.Getpid() == 1
|
||||
}
|
||||
|
||||
func catchSignals(logger slog.Logger, pid int, sigs []os.Signal) {
|
||||
if len(sigs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, sigs...)
|
||||
defer signal.Stop(sc)
|
||||
|
||||
logger.Info(context.Background(), "reaper catching signals",
|
||||
slog.F("signals", sigs),
|
||||
slog.F("child_pid", pid),
|
||||
)
|
||||
|
||||
func catchSignals(logger slog.Logger, pid int, sc <-chan os.Signal) {
|
||||
for {
|
||||
s := <-sc
|
||||
sig, ok := s.(syscall.Signal)
|
||||
@@ -64,10 +52,17 @@ func ForkReap(opt ...Option) (int, error) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
go reap.ReapChildren(opts.PIDs, nil, opts.Done, nil)
|
||||
// Use the reapLock to prevent the reaper's Wait4(-1) from
|
||||
// stealing the direct child's exit status. The reaper takes
|
||||
// a write lock; we hold a read lock during our own Wait4.
|
||||
var reapLock sync.RWMutex
|
||||
reapLock.RLock()
|
||||
|
||||
go reap.ReapChildren(opts.PIDs, nil, opts.Done, &reapLock)
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
reapLock.RUnlock()
|
||||
return 1, xerrors.Errorf("get wd: %w", err)
|
||||
}
|
||||
|
||||
@@ -87,16 +82,26 @@ func ForkReap(opt ...Option) (int, error) {
|
||||
//#nosec G204
|
||||
pid, err := syscall.ForkExec(opts.ExecArgs[0], opts.ExecArgs, pattrs)
|
||||
if err != nil {
|
||||
reapLock.RUnlock()
|
||||
return 1, xerrors.Errorf("fork exec: %w", err)
|
||||
}
|
||||
|
||||
go catchSignals(opts.Logger, pid, opts.CatchSignals)
|
||||
// Register the signal handler before spawning the goroutine
|
||||
// so it is active by the time the child process starts. This
|
||||
// avoids a race where a signal arrives before the goroutine
|
||||
// has called signal.Notify.
|
||||
if len(opts.CatchSignals) > 0 {
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, opts.CatchSignals...)
|
||||
go catchSignals(opts.Logger, pid, sc)
|
||||
}
|
||||
|
||||
var wstatus syscall.WaitStatus
|
||||
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
|
||||
for xerrors.Is(err, syscall.EINTR) {
|
||||
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
|
||||
}
|
||||
reapLock.RUnlock()
|
||||
|
||||
// Convert wait status to exit code using standard Unix conventions:
|
||||
// - Normal exit: use the exit code
|
||||
|
||||
@@ -599,8 +599,11 @@ func TestExecutorAutostopAIAgentActivity(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: the autobuild executor ticks after the bumped deadline.
|
||||
// Use time.Now() to account for elapsed time since the test's
|
||||
// "now" variable, because the activity bump uses the database
|
||||
// NOW() which advances with wall clock time.
|
||||
go func() {
|
||||
tickTime := now.Add(time.Hour).Add(time.Minute)
|
||||
tickTime := time.Now().Add(time.Hour).Add(time.Minute)
|
||||
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
|
||||
tickCh <- tickTime
|
||||
close(tickCh)
|
||||
|
||||
+78
-4
@@ -74,7 +74,13 @@ export async function login(page: Page, options: LoginOptions = users.owner) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: reset the current user
|
||||
(ctx as any)[Symbol.for("currentUser")] = undefined;
|
||||
await ctx.clearCookies();
|
||||
await page.goto("/login");
|
||||
await page.goto("/login", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// Dynamic imports can fail with ERR_NETWORK_CHANGED during
|
||||
// parallel test execution. Reload the page if the error
|
||||
// boundary appears instead of the login form.
|
||||
await reloadPageIfDynamicImportFailed(page, "form");
|
||||
|
||||
await page.getByLabel("Email").fill(options.email);
|
||||
await page.getByLabel("Password").fill(options.password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
@@ -126,6 +132,11 @@ export const createWorkspace = async (
|
||||
});
|
||||
await expectUrl(page).toHavePathName(`/templates/${templatePath}/workspace`);
|
||||
|
||||
// Dynamic imports can fail with ERR_NETWORK_CHANGED during
|
||||
// parallel test execution. Reload the page if the error
|
||||
// boundary appears instead of the form.
|
||||
await reloadPageIfDynamicImportFailed(page, "form");
|
||||
|
||||
const name = randomName();
|
||||
await page.getByLabel("name").fill(name);
|
||||
|
||||
@@ -211,8 +222,18 @@ export const verifyParameters = async (
|
||||
case "bool":
|
||||
{
|
||||
const parameterField = parameterLabel.locator("input");
|
||||
const value = await parameterField.isChecked();
|
||||
expect(value.toString()).toEqual(buildParameter.value);
|
||||
// Dynamic parameters can hydrate after initial render
|
||||
// and reset checkbox state. Use auto-retrying assertions
|
||||
// instead of a one-shot isChecked() snapshot.
|
||||
if (buildParameter.value === "true") {
|
||||
await expect(parameterField).toBeChecked({
|
||||
timeout: 15_000,
|
||||
});
|
||||
} else {
|
||||
await expect(parameterField).not.toBeChecked({
|
||||
timeout: 15_000,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
@@ -815,6 +836,37 @@ export const randomName = (annotation?: string) => {
|
||||
return annotation ? `${annotation}-${base}` : base;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload the page if a dynamic import failed (e.g. due to
|
||||
* ERR_NETWORK_CHANGED). When React Router catches a failed chunk
|
||||
* load it renders the GlobalErrorBoundary with the heading
|
||||
* "Something went wrong". Detecting that and reloading is enough
|
||||
* to recover.
|
||||
*/
|
||||
async function reloadPageIfDynamicImportFailed(
|
||||
page: Page,
|
||||
expectedSelector: string,
|
||||
) {
|
||||
const errorHeading = page.getByRole("heading", {
|
||||
name: "Something went wrong",
|
||||
});
|
||||
// Race the expected content against the error boundary. Suppress
|
||||
// the timeout rejection from whichever side loses.
|
||||
const result = await Promise.race([
|
||||
page
|
||||
.waitForSelector(expectedSelector, { timeout: 10_000 })
|
||||
.then(() => "ok" as const)
|
||||
.catch(() => "timeout" as const),
|
||||
errorHeading
|
||||
.waitFor({ state: "visible", timeout: 10_000 })
|
||||
.then(() => "error" as const)
|
||||
.catch(() => "timeout" as const),
|
||||
]);
|
||||
if (result === "error") {
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaiter is a helper that allows you to wait for a callback to be called. It
|
||||
* is useful for waiting for events to occur.
|
||||
@@ -1051,8 +1103,30 @@ const fillParameters = async (
|
||||
switch (richParameter.type) {
|
||||
case "bool":
|
||||
{
|
||||
const wantChecked = buildParameter.value === "true";
|
||||
const parameterField = parameterLabel.locator("button");
|
||||
await parameterField.click();
|
||||
// Dynamic parameters can hydrate after initial render
|
||||
// and reset the switch state. Check the current value
|
||||
// and retry the click if needed.
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const isChecked =
|
||||
(await parameterField.getAttribute("aria-checked")) === "true";
|
||||
if (isChecked !== wantChecked) {
|
||||
await parameterField.click();
|
||||
}
|
||||
try {
|
||||
await expect(parameterField).toHaveAttribute(
|
||||
"aria-checked",
|
||||
String(wantChecked),
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (attempt === 2) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
|
||||
Reference in New Issue
Block a user