diff --git a/internal/jvm/agent_provider_test.go b/internal/jvm/agent_provider_test.go index 567ffa16..770d0999 100644 --- a/internal/jvm/agent_provider_test.go +++ b/internal/jvm/agent_provider_test.go @@ -1,7 +1,6 @@ package jvm import ( - "bufio" "context" "encoding/json" "fmt" @@ -105,16 +104,6 @@ func TestAgentProviderGetMonitoringSnapshotDecodesResponse(t *testing.T) { } func TestAgentProviderRealAgentRoundTrip(t *testing.T) { - if _, err := exec.LookPath("java"); err != nil { - t.Skipf("java 不可用,跳过真实 Agent 集成测试: %v", err) - } - if _, err := exec.LookPath("javac"); err != nil { - t.Skipf("javac 不可用,跳过真实 Agent 集成测试: %v", err) - } - if _, err := exec.LookPath("jar"); err != nil { - t.Skipf("jar 不可用,跳过真实 Agent 集成测试: %v", err) - } - provider := NewAgentProvider() fixture := startAgentFixture(t) cfg := newAgentProviderTestConfig(fixture.baseURL+"/gonavi/agent/jvm", 5) @@ -212,24 +201,12 @@ func TestAgentProviderRealAgentRoundTrip(t *testing.T) { type agentFixtureProcess struct { port int baseURL string - cmd *exec.Cmd } func startAgentFixture(t *testing.T) agentFixtureProcess { t.Helper() - javaBin, err := exec.LookPath("java") - if err != nil { - t.Fatalf("look up java failed: %v", err) - } - javacBin, err := exec.LookPath("javac") - if err != nil { - t.Fatalf("look up javac failed: %v", err) - } - jarBin, err := exec.LookPath("jar") - if err != nil { - t.Fatalf("look up jar failed: %v", err) - } + toolchain := requireJVMFixtureToolchain(t, true) classesDir := filepath.Join(t.TempDir(), "agent-fixture-classes") if err := os.MkdirAll(classesDir, 0o755); err != nil { @@ -244,12 +221,7 @@ func startAgentFixture(t *testing.T) agentFixtureProcess { t.Fatalf("expected agent fixture java files under %s", sourceRoot) } - compileArgs := append([]string{"-encoding", "UTF-8", "-d", classesDir}, javaFiles...) - compileCmd := exec.Command(javacBin, compileArgs...) - output, err := compileCmd.CombinedOutput() - if err != nil { - t.Fatalf("compile agent fixture failed: %v\n%s", err, strings.TrimSpace(string(output))) - } + compileJVMFixture(t, toolchain, "agent", classesDir, javaFiles) manifestPath := filepath.Join(t.TempDir(), "agent-manifest.mf") manifest := strings.Join([]string{ @@ -275,60 +247,23 @@ func startAgentFixture(t *testing.T) agentFixtureProcess { t.Cleanup(func() { _ = os.Remove(agentJar) }) - jarCmd := exec.Command(jarBin, "cmf", manifestPath, agentJar, "-C", classesDir, "com") - output, err = jarCmd.CombinedOutput() + jarCmd := exec.Command(toolchain.JarBin, "cmf", manifestPath, agentJar, "-C", classesDir, "com") + output, err := jarCmd.CombinedOutput() if err != nil { - t.Fatalf("package agent jar failed: %v\n%s", err, strings.TrimSpace(string(output))) + t.Fatalf("package agent jar failed: %v; output: %s; toolchain: %s", err, nonEmptyJVMFixtureText(strings.TrimSpace(string(output)), ""), toolchain.summary()) } port := reserveTCPPort(t) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - cmd := exec.CommandContext( - ctx, - javaBin, + process, stdout := startJVMFixtureCommand( + t, + toolchain, + "agent", fmt.Sprintf("-javaagent:%s=port=%d,token=secret-token", agentJar, port), "-cp", classesDir, "com.gonavi.fixture.AgentHostApp", ) - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatalf("agent fixture stdout pipe failed: %v", err) - } - if err := cmd.Start(); err != nil { - t.Fatalf("start agent fixture failed: %v", err) - } - t.Cleanup(func() { - cancel() - _ = cmd.Wait() - }) - - ready := make(chan error, 1) - go func() { - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - if strings.TrimSpace(scanner.Text()) == "AGENT_READY" { - ready <- nil - return - } - } - if err := scanner.Err(); err != nil { - ready <- fmt.Errorf("agent fixture readiness read failed: %w", err) - return - } - ready <- fmt.Errorf("agent fixture terminated before readiness signal") - }() - - select { - case err := <-ready: - if err != nil { - t.Fatalf("wait agent fixture ready failed: %v", err) - } - case <-time.After(20 * time.Second): - t.Fatal("agent fixture did not become ready within 20s") - } + waitForJVMFixtureReady(t, process, stdout, toolchain, "agent", "AGENT_READY", 20*time.Second) waitForTest(t, 10*time.Second, func() error { conn, dialErr := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond) @@ -342,7 +277,6 @@ func startAgentFixture(t *testing.T) agentFixtureProcess { return agentFixtureProcess{ port: port, baseURL: fmt.Sprintf("http://127.0.0.1:%d", port), - cmd: cmd, } } diff --git a/internal/jvm/fixture_toolchain_test.go b/internal/jvm/fixture_toolchain_test.go new file mode 100644 index 00000000..447f08b2 --- /dev/null +++ b/internal/jvm/fixture_toolchain_test.go @@ -0,0 +1,448 @@ +package jvm + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" +) + +var errJVMFixtureToolchainUnavailable = errors.New("JDK toolchain unavailable") + +var jvmFixtureVersionPattern = regexp.MustCompile(`(?i)(?:java|openjdk|javac)(?:\s+version)?\s+"?([0-9]+)(?:\.([0-9]+))?`) + +type jvmFixtureToolchain struct { + JavaBin string + JavacBin string + JarBin string + JavaVersion string + JavacVersion string + Major int +} + +func (toolchain jvmFixtureToolchain) summary() string { + return fmt.Sprintf( + "java=%s (%s), javac=%s (%s), major=%d", + toolchain.JavaBin, + compactJVMFixtureVersion(toolchain.JavaVersion), + toolchain.JavacBin, + compactJVMFixtureVersion(toolchain.JavacVersion), + toolchain.Major, + ) +} + +type jvmFixtureToolchainResolver struct { + getenv func(string) string + lookPath func(string) (string, error) + stat func(string) (os.FileInfo, error) + version func(context.Context, string) (string, int, error) +} + +func defaultJVMFixtureToolchainResolver() jvmFixtureToolchainResolver { + return jvmFixtureToolchainResolver{ + getenv: os.Getenv, + lookPath: exec.LookPath, + stat: os.Stat, + version: readJVMFixtureToolVersion, + } +} + +func requireJVMFixtureToolchain(t *testing.T, requireJar bool) jvmFixtureToolchain { + t.Helper() + + toolchain, err := defaultJVMFixtureToolchainResolver().resolve(context.Background(), requireJar) + if errors.Is(err, errJVMFixtureToolchainUnavailable) { + t.Skipf("一致 JDK 工具链不可用,跳过真实 JVM fixture 测试: %v", err) + } + if err != nil { + t.Fatalf("解析 JVM fixture JDK 工具链失败: %v", err) + } + return toolchain +} + +func (resolver jvmFixtureToolchainResolver) resolve(ctx context.Context, requireJar bool) (jvmFixtureToolchain, error) { + if javaHome := strings.TrimSpace(resolver.getenv("JAVA_HOME")); javaHome != "" { + binDir := filepath.Join(javaHome, "bin") + if toolchain, ok, err := resolver.resolveBinDir(ctx, binDir, requireJar); ok || err != nil { + return toolchain, err + } + } + + javacBin, err := resolver.lookPath(jvmFixtureBinaryName("javac")) + if err != nil { + return jvmFixtureToolchain{}, fmt.Errorf("%w: javac not found on PATH: %v", errJVMFixtureToolchainUnavailable, err) + } + binDir := filepath.Dir(javacBin) + if toolchain, ok, resolveErr := resolver.resolveBinDir(ctx, binDir, requireJar); ok || resolveErr != nil { + return toolchain, resolveErr + } + + javaBin, err := resolver.lookPath(jvmFixtureBinaryName("java")) + if err != nil { + return jvmFixtureToolchain{}, fmt.Errorf("%w: java not found beside %s or on PATH: %v", errJVMFixtureToolchainUnavailable, javacBin, err) + } + jarBin := "" + if requireJar { + return jvmFixtureToolchain{}, fmt.Errorf("JDK toolchain beside %s does not contain %s", javacBin, jvmFixtureBinaryName("jar")) + } + return resolver.validate(ctx, javaBin, javacBin, jarBin) +} + +func (resolver jvmFixtureToolchainResolver) resolveBinDir(ctx context.Context, binDir string, requireJar bool) (jvmFixtureToolchain, bool, error) { + javaBin := filepath.Join(binDir, jvmFixtureBinaryName("java")) + javacBin := filepath.Join(binDir, jvmFixtureBinaryName("javac")) + if !resolver.isFile(javaBin) || !resolver.isFile(javacBin) { + return jvmFixtureToolchain{}, false, nil + } + jarBin := "" + if requireJar { + jarBin = filepath.Join(binDir, jvmFixtureBinaryName("jar")) + if !resolver.isFile(jarBin) { + return jvmFixtureToolchain{}, false, nil + } + } + toolchain, err := resolver.validate(ctx, javaBin, javacBin, jarBin) + return toolchain, true, err +} + +func (resolver jvmFixtureToolchainResolver) isFile(path string) bool { + info, err := resolver.stat(path) + return err == nil && !info.IsDir() +} + +func (resolver jvmFixtureToolchainResolver) validate(ctx context.Context, javaBin, javacBin, jarBin string) (jvmFixtureToolchain, error) { + javaVersion, javaMajor, err := resolver.version(ctx, javaBin) + if err != nil { + return jvmFixtureToolchain{}, fmt.Errorf("read java version from %s: %w", javaBin, err) + } + javacVersion, javacMajor, err := resolver.version(ctx, javacBin) + if err != nil { + return jvmFixtureToolchain{}, fmt.Errorf("read javac version from %s: %w", javacBin, err) + } + if javaMajor != javacMajor { + return jvmFixtureToolchain{}, fmt.Errorf( + "java/javac major version mismatch: java=%s (%s, major=%d), javac=%s (%s, major=%d)", + javaBin, + compactJVMFixtureVersion(javaVersion), + javaMajor, + javacBin, + compactJVMFixtureVersion(javacVersion), + javacMajor, + ) + } + return jvmFixtureToolchain{ + JavaBin: javaBin, + JavacBin: javacBin, + JarBin: jarBin, + JavaVersion: javaVersion, + JavacVersion: javacVersion, + Major: javaMajor, + }, nil +} + +func jvmFixtureBinaryName(name string) string { + if runtime.GOOS == "windows" { + return name + ".exe" + } + return name +} + +func readJVMFixtureToolVersion(parent context.Context, binary string) (string, int, error) { + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + + output, err := exec.CommandContext(ctx, binary, "-version").CombinedOutput() + text := strings.TrimSpace(string(output)) + if err != nil { + return text, 0, fmt.Errorf("%w; output: %s", err, nonEmptyJVMFixtureText(text, "")) + } + major, err := parseJVMFixtureMajorVersion(text) + if err != nil { + return text, 0, err + } + return text, major, nil +} + +func parseJVMFixtureMajorVersion(output string) (int, error) { + matches := jvmFixtureVersionPattern.FindStringSubmatch(output) + if len(matches) < 2 { + return 0, fmt.Errorf("unrecognized Java version output: %q", compactJVMFixtureVersion(output)) + } + major, err := strconv.Atoi(matches[1]) + if err != nil { + return 0, fmt.Errorf("parse Java major version from %q: %w", matches[1], err) + } + if major == 1 && len(matches) > 2 && matches[2] != "" { + major, err = strconv.Atoi(matches[2]) + if err != nil { + return 0, fmt.Errorf("parse legacy Java major version from %q: %w", matches[2], err) + } + } + return major, nil +} + +func compactJVMFixtureVersion(version string) string { + return strings.Join(strings.Fields(version), " ") +} + +func nonEmptyJVMFixtureText(value, fallback string) string { + if strings.TrimSpace(value) == "" { + return fallback + } + return value +} + +func compileJVMFixture(t *testing.T, toolchain jvmFixtureToolchain, label, classesDir string, javaFiles []string) { + t.Helper() + + compileArgs := append([]string{"-encoding", "UTF-8", "-d", classesDir}, javaFiles...) + output, err := exec.Command(toolchain.JavacBin, compileArgs...).CombinedOutput() + if err != nil { + t.Fatalf("compile %s fixture failed: %v; output: %s; toolchain: %s", label, err, nonEmptyJVMFixtureText(strings.TrimSpace(string(output)), ""), toolchain.summary()) + } +} + +type jvmFixtureCommand struct { + cmd *exec.Cmd + cancel context.CancelFunc + stderr bytes.Buffer + waitOnce sync.Once + waitErr error +} + +func startJVMFixtureCommand(t *testing.T, toolchain jvmFixtureToolchain, label string, args ...string) (*jvmFixtureCommand, io.ReadCloser) { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, toolchain.JavaBin, args...) + stdout, err := cmd.StdoutPipe() + if err != nil { + cancel() + t.Fatalf("%s fixture stdout pipe failed: %v; toolchain: %s", label, err, toolchain.summary()) + } + process := &jvmFixtureCommand{cmd: cmd, cancel: cancel} + cmd.Stderr = &process.stderr + if err := cmd.Start(); err != nil { + cancel() + t.Fatalf("start %s fixture failed: %v; toolchain: %s", label, err, toolchain.summary()) + } + t.Cleanup(func() { + process.stopAndWait() + }) + return process, stdout +} + +func (process *jvmFixtureCommand) stopAndWait() error { + process.cancel() + process.waitOnce.Do(func() { + process.waitErr = process.cmd.Wait() + }) + return process.waitErr +} + +func waitForJVMFixtureReady(t *testing.T, process *jvmFixtureCommand, stdout io.Reader, toolchain jvmFixtureToolchain, label, expectedLine string, timeout time.Duration) { + t.Helper() + + ready := make(chan error, 1) + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + if strings.TrimSpace(scanner.Text()) == expectedLine { + ready <- nil + return + } + } + if err := scanner.Err(); err != nil { + ready <- fmt.Errorf("readiness read failed: %w", err) + return + } + ready <- io.EOF + }() + + select { + case err := <-ready: + if err != nil { + waitErr := process.stopAndWait() + t.Fatalf("%v", formatJVMFixtureStartError(label, err, waitErr, process.stderr.String(), toolchain)) + } + case <-time.After(timeout): + waitErr := process.stopAndWait() + t.Fatalf("%v", formatJVMFixtureStartError(label, fmt.Errorf("did not become ready within %s", timeout), waitErr, process.stderr.String(), toolchain)) + } +} + +func formatJVMFixtureStartError(label string, cause, waitErr error, stderr string, toolchain jvmFixtureToolchain) error { + exitStatus := "success" + if waitErr != nil { + exitStatus = waitErr.Error() + } + return fmt.Errorf( + "%s fixture readiness failed: %w; exit status: %s; stderr: %s; toolchain: %s", + label, + cause, + exitStatus, + nonEmptyJVMFixtureText(strings.TrimSpace(stderr), ""), + toolchain.summary(), + ) +} + +func TestJVMFixtureToolchainUsesJavaBesideJavac(t *testing.T) { + binDir := t.TempDir() + javacBin := filepath.Join(binDir, jvmFixtureBinaryName("javac")) + siblingJava := filepath.Join(binDir, jvmFixtureBinaryName("java")) + pathJava8 := filepath.Join(t.TempDir(), jvmFixtureBinaryName("java")) + for _, path := range []string{javacBin, siblingJava, pathJava8} { + if err := os.WriteFile(path, []byte("fixture"), 0o755); err != nil { + t.Fatalf("write fake JVM tool %s: %v", path, err) + } + } + + resolver := fakeJVMFixtureToolchainResolver(t, "", map[string]string{ + jvmFixtureBinaryName("javac"): javacBin, + jvmFixtureBinaryName("java"): pathJava8, + }, map[string]struct { + version string + major int + }{ + javacBin: {version: "javac 17.0.15", major: 17}, + siblingJava: {version: `openjdk version "17.0.15"`, major: 17}, + pathJava8: {version: `java version "1.8.0_431"`, major: 8}, + }) + + toolchain, err := resolver.resolve(context.Background(), false) + if err != nil { + t.Fatalf("resolve() error = %v", err) + } + if toolchain.JavaBin != siblingJava || toolchain.JavacBin != javacBin || toolchain.Major != 17 { + t.Fatalf("resolve() = %#v, want javac sibling toolchain", toolchain) + } +} + +func TestJVMFixtureToolchainPrefersCompleteJavaHome(t *testing.T) { + javaHome := t.TempDir() + binDir := filepath.Join(javaHome, "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + t.Fatalf("create fake JAVA_HOME bin: %v", err) + } + javaBin := filepath.Join(binDir, jvmFixtureBinaryName("java")) + javacBin := filepath.Join(binDir, jvmFixtureBinaryName("javac")) + jarBin := filepath.Join(binDir, jvmFixtureBinaryName("jar")) + for _, path := range []string{javaBin, javacBin, jarBin} { + if err := os.WriteFile(path, []byte("fixture"), 0o755); err != nil { + t.Fatalf("write fake JAVA_HOME tool %s: %v", path, err) + } + } + + resolver := fakeJVMFixtureToolchainResolver(t, javaHome, map[string]string{}, map[string]struct { + version string + major int + }{ + javaBin: {version: `openjdk version "21.0.7"`, major: 21}, + javacBin: {version: "javac 21.0.7", major: 21}, + }) + + toolchain, err := resolver.resolve(context.Background(), true) + if err != nil { + t.Fatalf("resolve() error = %v", err) + } + if toolchain.JavaBin != javaBin || toolchain.JavacBin != javacBin || toolchain.JarBin != jarBin || toolchain.Major != 21 { + t.Fatalf("resolve() = %#v, want JAVA_HOME toolchain", toolchain) + } +} + +func TestJVMFixtureToolchainRejectsMismatchedFallback(t *testing.T) { + binDir := t.TempDir() + javacBin := filepath.Join(binDir, jvmFixtureBinaryName("javac")) + pathJava8 := filepath.Join(t.TempDir(), jvmFixtureBinaryName("java")) + for _, path := range []string{javacBin, pathJava8} { + if err := os.WriteFile(path, []byte("fixture"), 0o755); err != nil { + t.Fatalf("write fake JVM tool %s: %v", path, err) + } + } + + resolver := fakeJVMFixtureToolchainResolver(t, "", map[string]string{ + jvmFixtureBinaryName("javac"): javacBin, + jvmFixtureBinaryName("java"): pathJava8, + }, map[string]struct { + version string + major int + }{ + javacBin: {version: "javac 17.0.15", major: 17}, + pathJava8: {version: `java version "1.8.0_431"`, major: 8}, + }) + + _, err := resolver.resolve(context.Background(), false) + if err == nil || !strings.Contains(err.Error(), "major version mismatch") || !strings.Contains(err.Error(), "major=8") || !strings.Contains(err.Error(), "major=17") { + t.Fatalf("resolve() error = %v, want explicit version mismatch", err) + } +} + +func TestParseJVMFixtureMajorVersion(t *testing.T) { + cases := map[string]int{ + `java version "1.8.0_431"`: 8, + `openjdk version "17.0.15"`: 17, + `javac 21.0.7`: 21, + } + for output, want := range cases { + if got, err := parseJVMFixtureMajorVersion(output); err != nil || got != want { + t.Fatalf("parseJVMFixtureMajorVersion(%q) = %d, %v; want %d", output, got, err, want) + } + } +} + +func TestFormatJVMFixtureStartErrorIncludesDiagnostics(t *testing.T) { + toolchain := jvmFixtureToolchain{ + JavaBin: `C:\jdk-17\bin\java.exe`, + JavacBin: `C:\jdk-17\bin\javac.exe`, + JavaVersion: `openjdk version "17.0.15"`, + JavacVersion: "javac 17.0.15", + Major: 17, + } + err := formatJVMFixtureStartError("endpoint", io.EOF, errors.New("exit status 1"), "UnsupportedClassVersionError", toolchain) + for _, want := range []string{"endpoint", "EOF", "exit status 1", "UnsupportedClassVersionError", "java.exe", "javac.exe", "17.0.15"} { + if !strings.Contains(err.Error(), want) { + t.Fatalf("diagnostic error %q missing %q", err, want) + } + } +} + +func fakeJVMFixtureToolchainResolver(t *testing.T, javaHome string, paths map[string]string, versions map[string]struct { + version string + major int +}) jvmFixtureToolchainResolver { + t.Helper() + return jvmFixtureToolchainResolver{ + getenv: func(name string) string { + if name == "JAVA_HOME" { + return javaHome + } + return "" + }, + lookPath: func(name string) (string, error) { + if path := paths[name]; path != "" { + return path, nil + } + return "", exec.ErrNotFound + }, + stat: os.Stat, + version: func(_ context.Context, binary string) (string, int, error) { + version, ok := versions[binary] + if !ok { + return "", 0, fmt.Errorf("unexpected version lookup: %s", binary) + } + return version.version, version.major, nil + }, + } +} diff --git a/internal/jvm/http_provider_test.go b/internal/jvm/http_provider_test.go index c89b8b08..7d058aaa 100644 --- a/internal/jvm/http_provider_test.go +++ b/internal/jvm/http_provider_test.go @@ -1,7 +1,6 @@ package jvm import ( - "bufio" "context" "encoding/json" "fmt" @@ -10,7 +9,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -409,13 +407,6 @@ func TestHTTPProviderReturnsTimeoutError(t *testing.T) { } func TestHTTPProviderRealEndpointRoundTrip(t *testing.T) { - if _, err := exec.LookPath("java"); err != nil { - t.Skipf("java 不可用,跳过真实 Endpoint 集成测试: %v", err) - } - if _, err := exec.LookPath("javac"); err != nil { - t.Skipf("javac 不可用,跳过真实 Endpoint 集成测试: %v", err) - } - provider := NewHTTPProvider() fixture := startEndpointFixture(t) cfg := newHTTPProviderTestConfig(fixture.baseURL+"/manage/jvm", 5) @@ -506,20 +497,12 @@ func TestHTTPProviderRealEndpointRoundTrip(t *testing.T) { type endpointFixtureProcess struct { port int baseURL string - cmd *exec.Cmd } func startEndpointFixture(t *testing.T) endpointFixtureProcess { t.Helper() - javaBin, err := exec.LookPath("java") - if err != nil { - t.Fatalf("look up java failed: %v", err) - } - javacBin, err := exec.LookPath("javac") - if err != nil { - t.Fatalf("look up javac failed: %v", err) - } + toolchain := requireJVMFixtureToolchain(t, false) classesDir := filepath.Join(t.TempDir(), "endpoint-fixture-classes") if err := os.MkdirAll(classesDir, 0o755); err != nil { @@ -534,52 +517,11 @@ func startEndpointFixture(t *testing.T) endpointFixtureProcess { t.Fatalf("expected endpoint fixture java files under %s", sourceRoot) } - compileArgs := append([]string{"-encoding", "UTF-8", "-d", classesDir}, javaFiles...) - compileCmd := exec.Command(javacBin, compileArgs...) - output, err := compileCmd.CombinedOutput() - if err != nil { - t.Fatalf("compile endpoint fixture failed: %v\n%s", err, strings.TrimSpace(string(output))) - } + compileJVMFixture(t, toolchain, "endpoint", classesDir, javaFiles) port := reserveTCPPort(t) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - cmd := exec.CommandContext(ctx, javaBin, "-cp", classesDir, "com.gonavi.fixture.EndpointTestServer", fmt.Sprintf("%d", port)) - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatalf("endpoint fixture stdout pipe failed: %v", err) - } - if err := cmd.Start(); err != nil { - t.Fatalf("start endpoint fixture failed: %v", err) - } - t.Cleanup(func() { - cancel() - _ = cmd.Wait() - }) - - ready := make(chan error, 1) - go func() { - line, readErr := bufio.NewReader(stdout).ReadString('\n') - if readErr != nil { - ready <- fmt.Errorf("endpoint fixture readiness read failed: %w", readErr) - return - } - if strings.TrimSpace(line) != "READY" { - ready <- fmt.Errorf("unexpected endpoint fixture readiness line: %q", strings.TrimSpace(line)) - return - } - ready <- nil - }() - - select { - case err := <-ready: - if err != nil { - t.Fatalf("wait endpoint fixture ready failed: %v", err) - } - case <-time.After(20 * time.Second): - t.Fatal("endpoint fixture did not become ready within 20s") - } + process, stdout := startJVMFixtureCommand(t, toolchain, "endpoint", "-cp", classesDir, "com.gonavi.fixture.EndpointTestServer", fmt.Sprintf("%d", port)) + waitForJVMFixtureReady(t, process, stdout, toolchain, "endpoint", "READY", 20*time.Second) waitForTest(t, 10*time.Second, func() error { conn, dialErr := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond) @@ -593,7 +535,6 @@ func startEndpointFixture(t *testing.T) endpointFixtureProcess { return endpointFixtureProcess{ port: port, baseURL: fmt.Sprintf("http://127.0.0.1:%d", port), - cmd: cmd, } } diff --git a/internal/jvm/jmx_provider_test.go b/internal/jvm/jmx_provider_test.go index ea407aa4..13ad80d9 100644 --- a/internal/jvm/jmx_provider_test.go +++ b/internal/jvm/jmx_provider_test.go @@ -1,13 +1,11 @@ package jvm import ( - "bufio" "context" "errors" "fmt" "net" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -280,13 +278,6 @@ func TestJMXProviderGetValueRejectsUnknownResourcePath(t *testing.T) { } func TestJMXProviderRealJMXRoundTrip(t *testing.T) { - if _, err := exec.LookPath("java"); err != nil { - t.Skipf("java 不可用,跳过真实 JMX 集成测试: %v", err) - } - if _, err := exec.LookPath("javac"); err != nil { - t.Skipf("javac 不可用,跳过真实 JMX 集成测试: %v", err) - } - provider := NewJMXProvider() monitoringProvider, ok := provider.(MonitoringCapableProvider) if !ok { @@ -760,20 +751,12 @@ func TestJMXProviderRealJMXRoundTrip(t *testing.T) { type jmxFixtureProcess struct { port int - cmd *exec.Cmd } func startJMXFixture(t *testing.T) jmxFixtureProcess { t.Helper() - javaBin, err := exec.LookPath("java") - if err != nil { - t.Fatalf("look up java failed: %v", err) - } - javacBin, err := exec.LookPath("javac") - if err != nil { - t.Fatalf("look up javac failed: %v", err) - } + toolchain := requireJVMFixtureToolchain(t, false) classesDir := filepath.Join(t.TempDir(), "fixture-classes") if err := os.MkdirAll(classesDir, 0o755); err != nil { @@ -788,18 +771,10 @@ func startJMXFixture(t *testing.T) jmxFixtureProcess { t.Fatalf("expected fixture java files under %s", sourceRoot) } - compileArgs := append([]string{"-encoding", "UTF-8", "-d", classesDir}, javaFiles...) - compileCmd := exec.Command(javacBin, compileArgs...) - output, err := compileCmd.CombinedOutput() - if err != nil { - t.Fatalf("compile fixture failed: %v\n%s", err, strings.TrimSpace(string(output))) - } + compileJVMFixture(t, toolchain, "JMX", classesDir, javaFiles) port := reserveTCPPort(t) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - cmd := exec.CommandContext(ctx, javaBin, + process, stdout := startJVMFixtureCommand(t, toolchain, "JMX", fmt.Sprintf("-Dcom.sun.management.jmxremote.port=%d", port), fmt.Sprintf("-Dcom.sun.management.jmxremote.rmi.port=%d", port), "-Dcom.sun.management.jmxremote.authenticate=false", @@ -810,40 +785,7 @@ func startJMXFixture(t *testing.T) jmxFixtureProcess { "-cp", classesDir, "com.gonavi.fixture.JMXTestServer", ) - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatalf("fixture stdout pipe failed: %v", err) - } - if err := cmd.Start(); err != nil { - t.Fatalf("start fixture failed: %v", err) - } - t.Cleanup(func() { - cancel() - _ = cmd.Wait() - }) - - ready := make(chan error, 1) - go func() { - line, readErr := bufio.NewReader(stdout).ReadString('\n') - if readErr != nil { - ready <- fmt.Errorf("fixture readiness read failed: %w", readErr) - return - } - if strings.TrimSpace(line) != "READY" { - ready <- fmt.Errorf("unexpected fixture readiness line: %q", strings.TrimSpace(line)) - return - } - ready <- nil - }() - - select { - case err := <-ready: - if err != nil { - t.Fatalf("wait fixture ready failed: %v", err) - } - case <-time.After(20 * time.Second): - t.Fatal("fixture did not become ready within 20s") - } + waitForJVMFixtureReady(t, process, stdout, toolchain, "JMX", "READY", 20*time.Second) waitForTest(t, 10*time.Second, func() error { conn, dialErr := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond) @@ -854,7 +796,7 @@ func startJMXFixture(t *testing.T) jmxFixtureProcess { return nil }) - return jmxFixtureProcess{port: port, cmd: cmd} + return jmxFixtureProcess{port: port} } func waitForTest(t *testing.T, timeout time.Duration, fn func() error) {