Files
kenaito-kopsc/internal/platform/hardware_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

365 lines
10 KiB
Go

package platform
import (
"errors"
"testing"
)
// fakeHardwareProbe builds a probe backed by an in-memory file table.
func fakeHardwareProbe(goos string, cpus int, files map[string]string) hardwareProbe {
return hardwareProbe{
GOOS: goos,
NumCPU: cpus,
ReadFile: func(path string) ([]byte, error) {
content, ok := files[path]
if !ok {
return nil, errors.New("no such file")
}
return []byte(content), nil
},
Run: func(name string, args ...string) ([]byte, error) {
if content, ok := files[name+" "+joinArgs(args)]; ok {
return []byte(content), nil
}
return nil, errors.New("command not found")
},
}
}
func joinArgs(args []string) string {
out := ""
for i, a := range args {
if i > 0 {
out += " "
}
out += a
}
return out
}
func TestDetectResourcesLinuxHost(t *testing.T) {
p := fakeHardwareProbe("linux", 8, map[string]string{
"/proc/meminfo": "MemTotal: 16384000 kB\nMemFree: 100 kB\n",
})
res := p.detect()
if res.CPUs != 8 {
t.Errorf("CPUs = %d, want 8", res.CPUs)
}
if want := int64(16384000) * 1024; res.Memory != want {
t.Errorf("Memory = %d, want %d", res.Memory, want)
}
if res.Detected != "/proc/meminfo" {
t.Errorf("Detected = %q", res.Detected)
}
}
func TestDetectResourcesLinuxCgroupV2Wins(t *testing.T) {
p := fakeHardwareProbe("linux", 64, map[string]string{
"/proc/meminfo": "MemTotal: 65536000 kB\n",
"/sys/fs/cgroup/memory.max": "4294967296\n", // 4 GiB
"/sys/fs/cgroup/cpu.max": "200000 100000\n", // 2 CPUs
})
res := p.detect()
if res.Memory != 4<<30 {
t.Errorf("Memory = %d, want 4GiB (cgroup limit must win over the host)", res.Memory)
}
if res.Detected != "cgroup v2 memory.max" {
t.Errorf("Detected = %q", res.Detected)
}
if res.CPUs != 2 {
t.Errorf("CPUs = %d, want 2 (cgroup quota)", res.CPUs)
}
}
func TestDetectResourcesLinuxCgroupUnlimited(t *testing.T) {
p := fakeHardwareProbe("linux", 4, map[string]string{
"/proc/meminfo": "MemTotal: 8192000 kB\n",
"/sys/fs/cgroup/memory.max": "max\n",
"/sys/fs/cgroup/cpu.max": "max 100000\n",
})
res := p.detect()
if want := int64(8192000) * 1024; res.Memory != want {
t.Errorf("Memory = %d, want the host total %d", res.Memory, want)
}
if res.CPUs != 4 {
t.Errorf("CPUs = %d, want the host count 4", res.CPUs)
}
}
func TestDetectResourcesLinuxCgroupV1(t *testing.T) {
p := fakeHardwareProbe("linux", 32, map[string]string{
"/proc/meminfo": "MemTotal: 33554432 kB\n",
"/sys/fs/cgroup/memory/memory.limit_in_bytes": "2147483648\n", // 2 GiB
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us": "150000\n",
"/sys/fs/cgroup/cpu/cpu.cfs_period_us": "100000\n",
})
res := p.detect()
if res.Memory != 2<<30 {
t.Errorf("Memory = %d, want 2GiB", res.Memory)
}
if res.CPUs != 2 {
t.Errorf("CPUs = %d, want 2 (1.5 CPU quota rounds up)", res.CPUs)
}
}
func TestDetectResourcesDarwin(t *testing.T) {
p := fakeHardwareProbe("darwin", 10, map[string]string{
"sysctl -n hw.memsize": "17179869184\n", // 16 GiB
})
res := p.detect()
if res.Memory != 16<<30 {
t.Errorf("Memory = %d, want 16GiB", res.Memory)
}
if res.Detected != "hw.memsize" || res.CPUs != 10 {
t.Errorf("res = %+v", res)
}
}
func TestDetectResourcesWindows(t *testing.T) {
p := fakeHardwareProbe("windows",
12,
map[string]string{
"wmic ComputerSystem get TotalPhysicalMemory /value": "\nTotalPhysicalMemory=8589934592\n\n",
})
res := p.detect()
if res.Memory != 8<<30 {
t.Errorf("Memory = %d, want 8GiB", res.Memory)
}
}
func TestDetectResourcesUnknownMemory(t *testing.T) {
p := fakeHardwareProbe("linux", 2, map[string]string{})
res := p.detect()
if res.Memory != 0 {
t.Errorf("Memory = %d, want 0 when nothing can be read", res.Memory)
}
if res.CPUs != 2 {
t.Errorf("CPUs = %d, want 2", res.CPUs)
}
}
func TestParseMemTotal(t *testing.T) {
if got := parseMemTotal("MemTotal: 16384000 kB\n"); got != int64(16384000)*1024 {
t.Errorf("parseMemTotal = %d", got)
}
if got := parseMemTotal("garbage\n"); got != 0 {
t.Errorf("parseMemTotal(garbage) = %d, want 0", got)
}
}
func TestParseCgroupLimit(t *testing.T) {
cases := []struct {
in string
want int64
ok bool
}{
{"max", 0, false},
{"", 0, false},
{"9223372036854771712", 0, false}, // cgroup v1 "unlimited"
{"4294967296", 4294967296, true},
}
for _, tc := range cases {
got, ok := parseCgroupLimit(tc.in)
if got != tc.want || ok != tc.ok {
t.Errorf("parseCgroupLimit(%q) = (%d, %v), want (%d, %v)", tc.in, got, ok, tc.want, tc.ok)
}
}
}
func TestCeilDiv(t *testing.T) {
cases := []struct {
a, b int64
want int
}{
{200000, 100000, 2},
{150000, 100000, 2},
{50000, 100000, 1},
}
for _, tc := range cases {
if got := ceilDiv(tc.a, tc.b); got != tc.want {
t.Errorf("ceilDiv(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
}
}
func TestLinuxCgroupCPUQuotaBeatsHostCores(t *testing.T) {
// A 64-core host with a container limited to 4 CPUs must report 4.
p := fakeHardwareProbe("linux", 64, map[string]string{
"/proc/meminfo": "MemTotal: 65536000 kB\n",
"/sys/fs/cgroup/cpu.max": "400000 100000\n",
})
if res := p.detect(); res.CPUs != 4 {
t.Fatalf("CPUs = %d, want 4 (cgroup quota)", res.CPUs)
}
}
func TestLinuxCgroupCPUFractionalQuotaRoundsUp(t *testing.T) {
// 50% of one CPU still needs one core's worth of threads.
p := fakeHardwareProbe("linux", 8, map[string]string{
"/proc/meminfo": "MemTotal: 8192000 kB\n",
"/sys/fs/cgroup/cpu.max": "50000 100000\n",
})
if res := p.detect(); res.CPUs != 1 {
t.Fatalf("CPUs = %d, want 1", res.CPUs)
}
}
func TestLinuxCgroupCPUUnlimitedKeepsHostCores(t *testing.T) {
p := fakeHardwareProbe("linux", 12, map[string]string{
"/proc/meminfo": "MemTotal: 8192000 kB\n",
"/sys/fs/cgroup/cpu.max": "max 100000\n",
})
if res := p.detect(); res.CPUs != 12 {
t.Fatalf("CPUs = %d, want the host count 12", res.CPUs)
}
}
func TestLinuxCgroupV1QuotaBeatsHostCores(t *testing.T) {
p := fakeHardwareProbe("linux", 32, map[string]string{
"/proc/meminfo": "MemTotal: 8192000 kB\n",
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us": "300000\n",
"/sys/fs/cgroup/cpu/cpu.cfs_period_us": "100000\n",
})
if res := p.detect(); res.CPUs != 3 {
t.Fatalf("CPUs = %d, want 3", res.CPUs)
}
}
func TestLinuxCgroupV1NegativeQuotaMeansUnlimited(t *testing.T) {
p := fakeHardwareProbe("linux", 6, map[string]string{
"/proc/meminfo": "MemTotal: 8192000 kB\n",
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us": "-1\n",
})
if res := p.detect(); res.CPUs != 6 {
t.Fatalf("CPUs = %d, want the host count 6 (quota -1 means unlimited)", res.CPUs)
}
}
// fakeJavaProbe returns canned output for specific java invocations.
func fakeJavaProbe(versionStderr, propsStderr string) javaProbe {
return javaProbe{
LookPath: func(name string) (string, error) {
if name != "java" {
return "", errors.New("not found")
}
return "/usr/bin/java", nil
},
Run: func(name string, args ...string) ([]byte, []byte, error) {
for _, a := range args {
if a == "-XshowSettings:properties" {
return nil, []byte(propsStderr), nil
}
}
return nil, []byte(versionStderr), nil
},
}
}
func TestDetectJavaVersions(t *testing.T) {
cases := []struct {
name string
banner string
want string
rawWant string
}{
{"jdk8", `java version "1.8.0_392"` + "\n", "8", "1.8.0_392"},
{"jdk11", `openjdk version "11.0.21" 2023-10-17`, "11", "11.0.21"},
{"jdk17", `openjdk version "17.0.9" 2023-10-17`, "17", "17.0.9"},
{"jdk21", `openjdk version "21.0.1" 2023-10-17 LTS`, "21", "21.0.1"},
{"vendor banner without 'version'", `openjdk 21.0.1 2023-10-17 LTS`, "21", "21.0.1"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
info := fakeJavaProbe(tc.banner, "").detectAt("/usr/bin/java")
if info.Version != tc.want {
t.Errorf("Version = %q, want %q", info.Version, tc.want)
}
if info.Raw != tc.rawWant {
t.Errorf("Raw = %q, want %q", info.Raw, tc.rawWant)
}
if info.Path != "/usr/bin/java" {
t.Errorf("Path = %q", info.Path)
}
})
}
}
func TestDetectJavaFallsBackToProperties(t *testing.T) {
// An unparsable banner: kopsc must use the properties dump instead.
props := " java.specification.version = 17\n java.version = 17.0.9\n"
info := fakeJavaProbe("weird banner without a version", props).detectAt("/opt/jdk/bin/java")
if info.Version != "17" {
t.Fatalf("Version = %q, want 17 from java.specification.version", info.Version)
}
}
func TestDetectJavaMissing(t *testing.T) {
probe := javaProbe{
LookPath: func(string) (string, error) { return "", errors.New("not found") },
}
if info := probe.detect(); info.Path != "" || info.Version != "" {
t.Fatalf("info = %+v, want empty when java is not installed", info)
}
}
func TestNormalizeJavaVersion(t *testing.T) {
cases := map[string]string{
"1.8.0_392": "8",
"1.8.0": "8",
"8": "8",
"11.0.21": "11",
"17": "17",
"21.0.1": "21",
"24.0.2": "24",
"": "",
"garbage": "",
}
for in, want := range cases {
if got := normalizeJavaVersion(in); got != want {
t.Errorf("normalizeJavaVersion(%q) = %q, want %q", in, got, want)
}
}
}
func TestParseJavaRelease(t *testing.T) {
cases := map[string]string{
`java version "1.8.0_392"`: "1.8.0_392",
`openjdk version "17.0.9"`: "17.0.9",
`openjdk 21.0.1 2023-10-17 LTS`: "21.0.1",
`no version here`: "",
}
for in, want := range cases {
if got := parseJavaRelease(in); got != want {
t.Errorf("parseJavaRelease(%q) = %q, want %q", in, got, want)
}
}
}
// TestProbesAreInjectable documents the contract that keeps these detectors
// testable: nothing in detect()/detectAt() touches the real host directly.
func TestProbesAreInjectable(t *testing.T) {
// A hardware probe with no dependencies wired must not panic and must
// report what the injected values say.
hw := hardwareProbe{
GOOS: "linux",
NumCPU: 3,
ReadFile: func(string) ([]byte, error) {
return nil, errors.New("denied")
},
}
if res := hw.detect(); res.CPUs != 3 || res.Memory != 0 {
t.Fatalf("injected probe = %+v, want CPUs 3 and unknown memory", res)
}
// A java probe whose LookPath and Run are both faked needs no JVM.
jp := javaProbe{
LookPath: func(string) (string, error) { return "/fake/java", nil },
Run: func(string, ...string) ([]byte, []byte, error) {
return nil, []byte(`openjdk version "21.0.1"`), nil
},
}
if info := jp.detect(); info.Version != "21" || info.Path != "/fake/java" {
t.Fatalf("injected java probe = %+v", info)
}
}