返回 DeepSeek-Reasonix
console_windows_test.go
根目录 / internal / desktoplauncher / console_windows_test.go
1 //go:build windows
2
3 package desktoplauncher
4
5 import (
6 "bytes"
7 "context"
8 _ "embed"
9 "encoding/binary"
10 "fmt"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "runtime"
15 "strings"
16 "syscall"
17 "testing"
18 "time"
19
20 "reasonix/internal/installlayout"
21 )
22
23 const consoleTestEnv = "REASONIX_LAUNCHER_CONSOLE_TEST"
24
25 //go:embed testdata/console-probe/main.go
26 var consoleProbeSource string
27
28 // TestMain lets a GUI copy of the test executable run the installed launch path.
29 func TestMain(m *testing.M) {
30 root := os.Getenv(consoleTestEnv)
31 if root == "" {
32 os.Exit(m.Run())
33 }
34 exe, err := os.Executable()
35 if err != nil {
36 panic(err)
37 }
38 name := filepath.Base(exe)
39 kernel := syscall.NewLazyDLL("kernel32.dll")
40 cp, _, _ := kernel.NewProc("GetConsoleCP").Call()
41 window, _, _ := kernel.NewProc("GetConsoleWindow").Call()
42 fmt.Printf("console-state %s %d %d\n", name, cp, window)
43 switch name {
44 case "reasonix-launcher.exe", "Reasonix.exe":
45 os.Exit(Run(os.Args[1:], "test"))
46 default:
47 panic("unexpected console test executable: " + name)
48 }
49 }
50
51 func TestLauncherDoesNotCreateConsoleWindow(t *testing.T) {
52 exe, err := os.Executable()
53 if err != nil {
54 t.Fatal(err)
55 }
56 executableBytes, err := os.ReadFile(exe)
57 if err != nil {
58 t.Fatal(err)
59 }
60 // Match the shipped -H windowsgui launcher while keeping its children CUI.
61 // A CREATE_NO_WINDOW console parent can pass an invisible console onward,
62 // masking the missing creation flag in the launcher under test.
63 guiBytes := bytes.Clone(executableBytes)
64 peOffset := binary.LittleEndian.Uint32(guiBytes[0x3c:0x40])
65 binary.LittleEndian.PutUint16(guiBytes[peOffset+24+68:], 2)
66 probeBytes := buildConsoleProbe(t)
67 for _, entry := range []string{"reasonix-launcher.exe", "Reasonix.exe"} {
68 for _, legacy := range []bool{false, true} {
69 t.Run(fmt.Sprintf("%s/legacy=%t", entry, legacy), func(t *testing.T) {
70 root := t.TempDir()
71 active := filepath.Join(root, "versions", "v1.0.0")
72 if err := os.MkdirAll(active, 0o755); err != nil {
73 t.Fatal(err)
74 }
75 paths := []string{filepath.Join(root, entry), filepath.Join(active, "reasonix-desktop.exe")}
76 if legacy {
77 paths = append(paths, filepath.Join(root, "reasonix-guard.exe"))
78 } else if err := writeConsoleTestPointer(root); err != nil {
79 t.Fatal(err)
80 }
81 for _, path := range paths {
82 data := probeBytes
83 if path == paths[0] {
84 data = guiBytes
85 }
86 if err := os.WriteFile(path, data, 0o755); err != nil {
87 t.Fatal(err)
88 }
89 }
90 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
91 defer cancel()
92 cmd := exec.CommandContext(ctx, paths[0])
93 cmd.Env = append(os.Environ(), consoleTestEnv+"="+root)
94 cmd.WaitDelay = 5 * time.Second
95 output, err := cmd.CombinedOutput()
96 if err != nil {
97 t.Fatalf("launcher: %v\n%s", err, output)
98 }
99 seen := make(map[string]bool)
100 for line := range bytes.SplitSeq(output, []byte{'\n'}) {
101 if !bytes.HasPrefix(line, []byte("console-state ")) {
102 continue
103 }
104 var name string
105 var cp, window uint64
106 if _, err := fmt.Sscanf(string(line), "console-state %s %d %d", &name, &cp, &window); err != nil {
107 t.Fatal(err)
108 }
109 seen[name] = true
110 // CREATE_NO_WINDOW still permits a console code page; the
111 // regression is creating a window, not having console I/O.
112 if window != 0 {
113 t.Errorf("%s created a console window: codepage=%d window=%d", name, cp, window)
114 }
115 }
116 for _, path := range paths {
117 if !seen[filepath.Base(path)] {
118 t.Errorf("missing process probe for %s: %s", filepath.Base(path), output)
119 }
120 }
121 if legacy {
122 if _, err := os.Stat(filepath.Join(root, "reasonix-guard.exe")); !os.IsNotExist(err) {
123 t.Errorf("completed legacy migrator was not removed: %v", err)
124 }
125 }
126 if strings.Contains(string(output), "error:") {
127 t.Fatalf("launcher reported an error: %s", output)
128 }
129 })
130 }
131 }
132 }
133
134 func writeConsoleTestPointer(root string) error {
135 return installlayout.WriteCurrent(root, installlayout.CurrentPointer{
136 SchemaVersion: 1, ActiveVersion: "v1.0.0", ActiveDir: "versions/v1.0.0",
137 })
138 }
139
140 func buildConsoleProbe(t *testing.T) []byte {
141 t.Helper()
142 dir := t.TempDir()
143 if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(consoleProbeSource), 0o644); err != nil {
144 t.Fatal(err)
145 }
146 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
147 defer cancel()
148 cmd := exec.CommandContext(ctx, "go", "build", "-o", "probe.exe", "main.go")
149 cmd.Dir = dir
150 cmd.Env = append(os.Environ(), "GOOS=windows", "GOARCH="+runtime.GOARCH, "CGO_ENABLED=0", "GO111MODULE=off")
151 if output, err := cmd.CombinedOutput(); err != nil {
152 t.Fatalf("build console probe: %v\n%s", err, output)
153 }
154 data, err := os.ReadFile(filepath.Join(dir, "probe.exe"))
155 if err != nil {
156 t.Fatal(err)
157 }
158 return data
159 }
160
160 lines GO