Files
kenaito-kopsc/internal/executor/javaflags_test.go
T
odboy f62c1f4872 feat: 新增 java run,按 JDK 版本自动套用调优参数运行 Java 应用
- 自动识别 JDK 8/11/17/21 与宿主机 CPU、可用内存;容器内以 cgroup
  限额为准;堆默认取可用内存 50%,-Xms 与 -Xmx 同值
- 按版本分支生成参数:JDK 8 用 -Xloggc 一组,JDK 9+ 用 -Xlog:;
  堆超过 30g 时自动省略压缩指针参数
- 支持 --jdk/--java/--heap/--xms/--env/--gc-log/--heap-dump-on-oom/
  --print-flags/--dry-run;jar 之后的参数原样交给应用
- 前台运行:stdio 直连、退出码透传、SIGINT/SIGTERM 转发给 JVM
- BuiltinRequest 增加 Stdin,使内置命令可以读取调用方输入
- 修正帮助中的用法行重复(自定义 Usage 不再追加通用透传尾巴)
- 文档:新增 README.java.md,主 README 补充导航与示例
2026-11-01 09:16:19 +08:00

237 lines
7.4 KiB
Go

package executor
import (
"strings"
"testing"
)
const gib = int64(1) << 30
func listFlags(flags []string) string { return strings.Join(flags, " ") }
func TestBuildJavaFlagsJDK21(t *testing.T) {
flags := BuildJavaFlags(javaTuning{
JDK: "21",
Memory: 8 * gib,
Env: "prod",
GCDir: "/boot/logs",
})
got := listFlags(flags)
for _, want := range []string{
"-server",
"-Xmx4g", "-Xms4g", // 50% of 8 GiB
"-Xss256k",
"-XX:MaxDirectMemorySize=1g", // a quarter of the heap
"-XX:+UseCompressedOops", "-XX:+UseCompressedClassPointers",
"-XX:+SegmentedCodeCache",
"-XX:+UseG1GC",
"-XX:+ExplicitGCInvokesConcurrent",
"-Xlog:gc*,safepoint:/boot/logs/gc.log:time,uptime:filecount=100,filesize=50M",
"-Djava.security.egd=file:/dev/./urandom",
"-Dconfig.file=application-prod.yaml",
} {
if !strings.Contains(got, want) {
t.Errorf("JDK 21 flags missing %q:\n%s", want, got)
}
}
// JDK 8-only spellings must not appear on modern JVMs.
for _, unwanted := range []string{"-Xloggc:", "-XX:+PrintGCDetails", "-verbose:gc"} {
if strings.Contains(got, unwanted) {
t.Errorf("JDK 21 flags must not contain %q:\n%s", unwanted, got)
}
}
}
func TestBuildJavaFlagsJDK8UsesXloggc(t *testing.T) {
flags := BuildJavaFlags(javaTuning{JDK: "8", Memory: 4 * gib, GCDir: "/var/log/app"})
got := listFlags(flags)
for _, want := range []string{
"-Xmx2g", "-Xms2g",
"-XX:+UseG1GC",
"-verbose:gc",
"-Xloggc:/var/log/app/gc.log",
"-XX:+PrintGCDetails",
"-XX:+PrintGCDateStamps",
"-XX:+UseGCLogFileRotation",
"-XX:NumberOfGCLogFiles=100",
"-XX:GCLogFileSize=50M",
} {
if !strings.Contains(got, want) {
t.Errorf("JDK 8 flags missing %q:\n%s", want, got)
}
}
// The decisive compatibility rule: -Xlog: makes JDK 8 refuse to start.
if strings.Contains(got, "-Xlog:") {
t.Fatalf("JDK 8 must not receive -Xlog: (it cannot parse it):\n%s", got)
}
}
func TestBuildJavaFlagsJDK11And17UseXLlog(t *testing.T) {
for _, jdk := range []string{"11", "17"} {
got := listFlags(BuildJavaFlags(javaTuning{JDK: jdk, Memory: 4 * gib, GCDir: "./logs"}))
if !strings.Contains(got, "-Xlog:gc*,safepoint:./logs/gc.log:time,uptime:filecount=100,filesize=50M") {
t.Errorf("JDK %s should use -Xlog:, got:\n%s", jdk, got)
}
if strings.Contains(got, "-Xloggc:") {
t.Errorf("JDK %s must not use -Xloggc:\n%s", jdk, got)
}
}
}
func TestBuildJavaFlagsOrder(t *testing.T) {
flags := BuildJavaFlags(javaTuning{JDK: "17", Memory: 2 * gib, GCDir: "/tmp"})
// The command line must start with the executable-adjacent flags so it can
// be pasted into a Dockerfile unchanged.
if flags[0] != "-server" {
t.Fatalf("first flag = %q, want -server", flags[0])
}
xmx, xms := indexOf(flags, "-Xmx1g"), indexOf(flags, "-Xms1g")
if xmx < 0 || xms < 0 || xmx > xms {
t.Errorf("-Xmx should come before -Xms (%d, %d): %v", xmx, xms, flags)
}
logIdx := indexOfPrefix(flags, "-Xlog:")
propertyIdx := indexOfPrefix(flags, "-Dconfig.file=")
if logIdx < 0 || propertyIdx < 0 || logIdx > propertyIdx {
t.Errorf("-Dconfig.file should come after the GC flags: %v", flags)
}
}
func indexOf(values []string, want string) int {
for i, v := range values {
if v == want {
return i
}
}
return -1
}
func indexOfPrefix(values []string, prefix string) int {
for i, v := range values {
if strings.HasPrefix(v, prefix) {
return i
}
}
return -1
}
func TestBuildJavaFlagsCommonTuning(t *testing.T) {
got := listFlags(BuildJavaFlags(javaTuning{JDK: "17", Memory: 4 * gib, GCDir: "/tmp"}))
for _, want := range []string{
"-server",
"-Xss256k",
"-XX:MaxDirectMemorySize=512m", // a quarter of the 2g heap
"-XX:+SegmentedCodeCache",
"-XX:+ExplicitGCInvokesConcurrent",
"-Djava.security.egd=file:/dev/./urandom",
} {
if !strings.Contains(got, want) {
t.Errorf("common flag %q missing:\n%s", want, got)
}
}
}
func TestBuildJavaFlagsDirectMemoryMinimum(t *testing.T) {
// A tiny heap must still leave 64 MiB for direct buffers (NIO).
got := listFlags(BuildJavaFlags(javaTuning{JDK: "17", Memory: 128 << 20}))
if !strings.Contains(got, "-XX:MaxDirectMemorySize=64m") {
t.Fatalf("direct memory floor missing:\n%s", got)
}
}
func TestBuildJavaFlagsHeapDefaultsToHalf(t *testing.T) {
cases := []struct {
memory int64
wantXmx string
wantXms string
}{
{16 * gib, "-Xmx8g", "-Xms8g"},
{1 * gib, "-Xmx512m", "-Xms512m"},
{512 << 20, "-Xmx256m", "-Xms256m"},
}
for _, tc := range cases {
flags := BuildJavaFlags(javaTuning{JDK: "21", Memory: tc.memory})
if got := indexOf(flags, tc.wantXmx); got < 0 {
t.Errorf("memory %d: want %s in %v", tc.memory, tc.wantXmx, flags)
}
if got := indexOf(flags, tc.wantXms); got < 0 {
t.Errorf("memory %d: -Xms must equal -Xmx (%s), got %v", tc.memory, tc.wantXms, flags)
}
}
}
func TestBuildJavaFlagsHeapOverrides(t *testing.T) {
// --heap / --xmx wins over the 50% default.
flags := BuildJavaFlags(javaTuning{JDK: "17", Memory: 16 * gib, Heap: "4g"})
if indexOf(flags, "-Xmx4g") < 0 || indexOf(flags, "-Xms4g") < 0 {
t.Fatalf("explicit heap not applied: %v", flags)
}
if indexOf(flags, "-Xmx8g") >= 0 {
t.Fatalf("default heap leaked alongside the override: %v", flags)
}
// --xms makes the initial heap differ, which is the only way to split them.
flags = BuildJavaFlags(javaTuning{JDK: "17", Memory: 16 * gib, Heap: "4g", Initial: "1g"})
if indexOf(flags, "-Xmx4g") < 0 || indexOf(flags, "-Xms1g") < 0 {
t.Fatalf("initial heap override not applied: %v", flags)
}
}
func TestBuildJavaFlagsNoMemoryMeansNoHeapFlags(t *testing.T) {
flags := BuildJavaFlags(javaTuning{JDK: "17"})
if indexOfPrefix(flags, "-Xmx") >= 0 || indexOfPrefix(flags, "-Xms") >= 0 {
t.Fatalf("unknown memory must leave the heap to JVM ergonomics: %v", flags)
}
}
func TestConfigFileResolution(t *testing.T) {
// --env wins.
if got := configFile("staging"); got != "application-staging.yaml" {
t.Errorf("configFile(staging) = %q", got)
}
// APP_ENV is the fallback.
t.Setenv("APP_ENV", "uat")
if got := configFile(""); got != "application-uat.yaml" {
t.Errorf("configFile(\"\") with APP_ENV=uat = %q", got)
}
// prod is the default when neither is set.
t.Setenv("APP_ENV", "")
if got := configFile(""); got != "application-prod.yaml" {
t.Errorf("configFile(\"\") default = %q", got)
}
}
func TestBuildJavaFlagsEnvFlagWinsOverAppEnv(t *testing.T) {
t.Setenv("APP_ENV", "uat")
flags := BuildJavaFlags(javaTuning{JDK: "17", Memory: 2 * gib, Env: "staging"})
if indexOf(flags, "-Dconfig.file=application-staging.yaml") < 0 {
t.Fatalf("--env must win over APP_ENV: %v", flags)
}
}
func TestCompressedOopsDroppedForLargeHeaps(t *testing.T) {
// Above 30g the JVM cannot use compressed oops, so kopsc stops emitting
// the flags instead of leaving a warning in the log.
big := BuildJavaFlags(javaTuning{JDK: "17", Heap: "32g"})
for _, flag := range big {
if flag == "-XX:+UseCompressedOops" || flag == "-XX:+UseCompressedClassPointers" {
t.Fatalf("heap 32g must drop compressed oops flags, got: %v", big)
}
}
// 30g is still inside the usable range.
ok := BuildJavaFlags(javaTuning{JDK: "17", Heap: "30g"})
if indexOf(ok, "-XX:+UseCompressedOops") < 0 {
t.Fatalf("heap 30g should keep compressed oops: %v", ok)
}
}
func TestCompressedOopsDroppedWhenDerivedHeapIsLarge(t *testing.T) {
// 128 GiB of memory yields a 64g heap by default, which is also too large.
flags := BuildJavaFlags(javaTuning{JDK: "21", Memory: 128 * gib})
if indexOf(flags, "-XX:+UseCompressedOops") >= 0 {
t.Fatalf("derived 64g heap must drop compressed oops: %v", flags)
}
}