test: close remaining workspace preprocessing gates
This commit is contained in:
@@ -3,9 +3,15 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestOwnedProcessUsesCreationTimeJobAttribute(t *testing.T) {
|
||||
@@ -24,3 +30,74 @@ func TestOwnedJobPolicyKillsDescendantsOnClose(t *testing.T) {
|
||||
t.Fatal("owned job must use kill-on-close policy")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsProcessHelper is both a test helper and the child process used by the runtime
|
||||
// ownership regression below. The child writes a marker only if it outlives its parent job.
|
||||
func TestWindowsProcessHelper(t *testing.T) {
|
||||
mode := os.Getenv("THOTHCTL_WINDOWS_HELPER")
|
||||
if mode == "" {
|
||||
return
|
||||
}
|
||||
marker := os.Getenv("THOTHCTL_WINDOWS_MARKER")
|
||||
if mode == "child" {
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = os.WriteFile(marker, []byte("survived"), 0o600)
|
||||
return
|
||||
}
|
||||
child := exec.Command(os.Args[0], "-test.run=TestWindowsProcessHelper", "--")
|
||||
child.Env = append(os.Environ(), "THOTHCTL_WINDOWS_HELPER=child", "THOTHCTL_WINDOWS_MARKER="+marker)
|
||||
if err := child.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode == "exit" {
|
||||
return
|
||||
}
|
||||
if mode == "cancel" {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
for {
|
||||
_, _ = os.Stdout.Write([]byte("overflow\n"))
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnedProcessTerminatesDescendantOnOverflowCancelAndParentExit(t *testing.T) {
|
||||
for _, mode := range []string{"overflow", "cancel", "exit"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
marker := filepath.Join(t.TempDir(), "descendant-marker")
|
||||
t.Setenv("THOTHCTL_WINDOWS_HELPER", mode)
|
||||
t.Setenv("THOTHCTL_WINDOWS_MARKER", marker)
|
||||
ctx := context.Background()
|
||||
var cancel context.CancelFunc
|
||||
if mode == "cancel" {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := runBoundedWindows(ctx, os.Args[0], []string{"-test.run=TestWindowsProcessHelper", "--"}, nil, CaptureLimits{StdoutBytes: 1024, StderrBytes: 1024})
|
||||
result <- err
|
||||
}()
|
||||
if mode == "cancel" {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
}
|
||||
select {
|
||||
case err := <-result:
|
||||
if mode == "overflow" && !errors.Is(err, ErrOutputLimit) {
|
||||
t.Fatalf("overflow error = %v, want ErrOutputLimit", err)
|
||||
}
|
||||
if mode == "cancel" && !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("cancel error = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("owned process did not terminate")
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("descendant marker error = %v; descendant survived %s termination", err, mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,16 @@ func TestReadCanonicalUTF8RejectsNonUTF8AndBounds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCanonicalUTF8RejectsSQLLargerThanOneMiB(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "schema.sql")
|
||||
if err := os.WriteFile(path, make([]byte, (1<<20)+1), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadCanonicalUTF8(path, 1<<20); !errors.Is(err, ErrUnsafeFile) {
|
||||
t.Fatalf("ReadCanonicalUTF8 1 MiB + 1 SQL = %v, want ErrUnsafeFile", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCanonicalExclusiveRejectsExistingAndCreatesPrivateFile(t *testing.T) {
|
||||
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
package safeio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
@@ -31,6 +33,56 @@ func TestReadCanonicalRegularRejectsNamedPipeWithoutBlocking(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCanonicalRegularRejectsReplacementDuringRead(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "schema.sql")
|
||||
replacement := filepath.Join(root, "replacement.sql")
|
||||
parked := filepath.Join(root, "parked.sql")
|
||||
contents := bytes.Repeat([]byte("x"), 64<<20)
|
||||
if err := os.WriteFile(path, contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(replacement, contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var caught bool
|
||||
for attempt := 0; attempt < 3 && !caught; attempt++ {
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := ReadCanonicalRegular(path, int64(len(contents)))
|
||||
result <- err
|
||||
}()
|
||||
time.Sleep(time.Millisecond)
|
||||
var finalErr error
|
||||
for i := 0; i < 20; i++ {
|
||||
if err := os.Rename(path, parked); err == nil {
|
||||
if err := os.Rename(replacement, path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
if err := os.Rename(parked, replacement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case finalErr = <-result:
|
||||
i = 20
|
||||
default:
|
||||
}
|
||||
}
|
||||
if finalErr == nil {
|
||||
finalErr = <-result
|
||||
}
|
||||
if errors.Is(finalErr, ErrUnsafeFile) {
|
||||
caught = true
|
||||
}
|
||||
}
|
||||
if !caught {
|
||||
t.Fatal("replacement during read was not rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalDescriptorOwnershipDoesNotLeakAcrossNestedOperations(t *testing.T) {
|
||||
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
|
||||
@@ -299,10 +299,28 @@ func TestParseWorkspaceAcceptsAssumptionAndEnforcesLimits(t *testing.T) {
|
||||
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", "orders.id=customers.id"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", strings.Repeat("a", maxAssumptionBytes+1)}); err == nil {
|
||||
exactAssumption := strings.Repeat("a", 127) + "=" + strings.Repeat("b", 128)
|
||||
if len(exactAssumption) != maxAssumptionBytes {
|
||||
t.Fatalf("test assumption length = %d, want %d", len(exactAssumption), maxAssumptionBytes)
|
||||
}
|
||||
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", exactAssumption}); err != nil {
|
||||
t.Fatalf("rejected exact assumption bound: %v", err)
|
||||
}
|
||||
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", exactAssumption + "a"}); err == nil {
|
||||
t.Fatal("accepted oversized assumption")
|
||||
}
|
||||
args := []string{"workspace", "schema", "suggest-fks", "--workspace", "psd"}
|
||||
for i := 0; i < maxAssumptions; i++ {
|
||||
args = append(args, "--assume", fmt.Sprintf("a%d=b%d", i, i))
|
||||
}
|
||||
if _, err := ParseWorkspaceCommand(args); err != nil {
|
||||
t.Fatalf("rejected exact assumption count: %v", err)
|
||||
}
|
||||
args = append(args, "--assume", "overflow=overflow")
|
||||
if _, err := ParseWorkspaceCommand(args); err == nil {
|
||||
t.Fatalf("accepted %d assumptions", maxAssumptions+1)
|
||||
}
|
||||
args = []string{"workspace", "schema", "suggest-fks", "--workspace", "psd"}
|
||||
for i := 0; i < 33; i++ {
|
||||
args = append(args, "--from-sql", fmt.Sprintf("schema-%d.sql", i))
|
||||
}
|
||||
@@ -368,7 +386,21 @@ func TestWorkspaceIngressAcceptsExactCountsAndByteBounds(t *testing.T) {
|
||||
for i := 0; i < maxAssumptions; i++ {
|
||||
assumptions = append(assumptions, fmt.Sprintf("column_%d=table_%d", i, i))
|
||||
}
|
||||
command := SuggestFksRequest{WorkspaceID: "psd", FromSQL: paths, Assume: assumptions}
|
||||
argv := []string{"workspace", "schema", "suggest-fks", "--workspace", "psd"}
|
||||
for _, path := range paths {
|
||||
argv = append(argv, "--from-sql", path)
|
||||
}
|
||||
for _, assumption := range assumptions {
|
||||
argv = append(argv, "--assume", assumption)
|
||||
}
|
||||
parsed, err := ParseWorkspaceCommand(argv)
|
||||
if err != nil {
|
||||
t.Fatalf("parser rejected exact file/assumption counts: %v", err)
|
||||
}
|
||||
command, ok := parsed.(SuggestFksRequest)
|
||||
if !ok {
|
||||
t.Fatalf("parsed command = %T, want SuggestFksRequest", parsed)
|
||||
}
|
||||
for _, path := range paths {
|
||||
if b, e := safeio.ReadCanonicalUTF8(path, maxSQLFile); e != nil {
|
||||
t.Fatalf("direct read %s: %v", path, e)
|
||||
|
||||
Reference in New Issue
Block a user