2026-08-16 01:27:30 +08:00
|
|
|
|
package backendupdate
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"archive/zip"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"log/slog"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
|
|
"yms-daemon/internal/deploymentconfig"
|
|
|
|
|
|
"yms-daemon/internal/hostnginx"
|
|
|
|
|
|
"yms-daemon/internal/nativebackendexecutor"
|
|
|
|
|
|
"yms-daemon/internal/systemd"
|
|
|
|
|
|
"yms-daemon/internal/transaction"
|
|
|
|
|
|
"yms-daemon/internal/updatepackage"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestUpdaterMigratesLegacyBackendAndCommits 验证通过 repack 包更新时,能把仍在运行的旧版后端单元
|
|
|
|
|
|
// (ymsback.service)迁移到模板单元并完成提交。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func TestUpdaterMigratesLegacyBackendAndCommits(t *testing.T) {
|
|
|
|
|
|
testUpdaterMigratesLegacyBackendAndCommits(t, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestUpdaterCommitsDirectNativeJAR 验证直接安装 native JAR 时能完成提交,
|
|
|
|
|
|
// 并进一步验证重启事务的创建、恢复与槽位旋转。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func TestUpdaterCommitsDirectNativeJAR(t *testing.T) {
|
|
|
|
|
|
testUpdaterMigratesLegacyBackendAndCommits(t, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// testUpdaterMigratesLegacyBackendAndCommits 上述两个测试共用的驱动函数。
|
|
|
|
|
|
// 当 direct 为 false 时走 repack 更新路径,为 true 时走直接 JAR 安装路径,
|
|
|
|
|
|
// 并额外验证重启事务的创建、恢复以及槽位旋转行为。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func testUpdaterMigratesLegacyBackendAndCommits(t *testing.T, direct bool) {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
|
store, err := transaction.OpenStore(ctx, filepath.Join(root, "transactions.db"))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("open transaction store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer store.Close()
|
|
|
|
|
|
coordinator, err := transaction.NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create transaction coordinator: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
releaseDir := filepath.Join(root, "releases")
|
|
|
|
|
|
activeJAR := filepath.Join(root, "lib", "glory-soft-yms.jar")
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(activeJAR), 0o750); err != nil {
|
|
|
|
|
|
t.Fatalf("create backend lib directory: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.WriteFile(activeJAR, []byte("legacy backend JAR"), 0o644); err != nil {
|
|
|
|
|
|
t.Fatalf("write legacy backend JAR: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
config := deploymentconfig.Config{Backend: deploymentconfig.Backend{
|
|
|
|
|
|
Type: deploymentconfig.BackendTypeNative,
|
|
|
|
|
|
ReleaseDir: releaseDir,
|
|
|
|
|
|
ActiveJAR: activeJAR,
|
|
|
|
|
|
Slot: deploymentconfig.BackendSlots{
|
|
|
|
|
|
Port8080: deploymentconfig.BackendSlot{
|
|
|
|
|
|
Unit: "yms-backend@8080.service",
|
|
|
|
|
|
JAR: filepath.Join(root, "lib", "glory-soft-yms-8080.jar"),
|
|
|
|
|
|
HealthEndpoint: "http://127.0.0.1:8080/yms/actuator/health",
|
|
|
|
|
|
},
|
|
|
|
|
|
Port8081: deploymentconfig.BackendSlot{
|
|
|
|
|
|
Unit: "yms-backend@8081.service",
|
|
|
|
|
|
JAR: filepath.Join(root, "lib", "glory-soft-yms-8081.jar"),
|
|
|
|
|
|
HealthEndpoint: "http://127.0.0.1:8081/yms/actuator/health",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
units := &updateUnitManager{units: map[string]systemd.Unit{
|
|
|
|
|
|
"yms-backend@8080.service": {Name: "yms-backend@8080.service", LoadState: "loaded", ActiveState: "inactive"},
|
|
|
|
|
|
"yms-backend@8081.service": {Name: "yms-backend@8081.service", LoadState: "loaded", ActiveState: "inactive"},
|
|
|
|
|
|
legacyUnit8080: {Name: legacyUnit8080, LoadState: "loaded", ActiveState: "failed"},
|
|
|
|
|
|
legacyUnit8081: {Name: legacyUnit8081, LoadState: "loaded", ActiveState: "activating"},
|
|
|
|
|
|
}}
|
|
|
|
|
|
gateway := &memoryGateway{snapshot: hostnginx.Snapshot{Content: []byte(serverConfiguration8081), ActivePort: 8081}}
|
|
|
|
|
|
executor := &preparingExecutor{store: store, releaseDir: releaseDir, units: units}
|
|
|
|
|
|
updater := &Updater{
|
|
|
|
|
|
config: config,
|
|
|
|
|
|
workRoot: filepath.Join(root, "work"),
|
|
|
|
|
|
store: store,
|
|
|
|
|
|
coordinator: coordinator,
|
|
|
|
|
|
units: units,
|
|
|
|
|
|
gateway: gateway,
|
|
|
|
|
|
executor: executor,
|
|
|
|
|
|
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
|
|
|
|
drain: 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
var record transaction.Transaction
|
|
|
|
|
|
var installedPath string
|
|
|
|
|
|
var progress []Progress
|
|
|
|
|
|
report := func(event Progress) {
|
|
|
|
|
|
progress = append(progress, event)
|
|
|
|
|
|
}
|
|
|
|
|
|
if direct {
|
|
|
|
|
|
jarPath := writeDirectBackendJAR(t, []byte("new backend JAR"))
|
|
|
|
|
|
jar, err := updatepackage.OpenDirectNativeJAR(jarPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("inspect direct native backend JAR: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
installedPath = filepath.Join(releaseDir, "direct", jar.SHA256[:12], jar.FileName)
|
|
|
|
|
|
record, err = updater.UpdateNativeJAR(ctx, jarPath, report)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
packagePath := writeNativeBackendPackage(t, []byte("new backend JAR"))
|
|
|
|
|
|
installedPath = filepath.Join(releaseDir, "glory-soft-yms-test.jar")
|
|
|
|
|
|
record, err = updater.UpdateRepack(ctx, packagePath, report)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("update native backend: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if record.State != transaction.StateCommitted || gateway.snapshot.ActivePort != 8080 {
|
|
|
|
|
|
t.Fatalf("unexpected committed update: record=%+v gateway=%+v", record, gateway.snapshot)
|
|
|
|
|
|
}
|
|
|
|
|
|
if target, err := os.Readlink(activeJAR); err != nil || target != installedPath {
|
|
|
|
|
|
t.Fatalf("unexpected compatibility link: target=%q installed=%q err=%v", target, installedPath, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if units.units[legacyUnit8081].ActiveState != "failed" || units.stopped != legacyUnit8081 {
|
|
|
|
|
|
t.Fatalf("legacy backend unit was not stopped: unit=%+v stopped=%s", units.units[legacyUnit8081], units.stopped)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(progress) == 0 || progress[len(progress)-1].State != transaction.StateCommitted {
|
|
|
|
|
|
t.Fatalf("missing committed update progress: %+v", progress)
|
|
|
|
|
|
}
|
|
|
|
|
|
if direct {
|
|
|
|
|
|
previousTransactionID := record.ID
|
|
|
|
|
|
restartSource, sourceErr := currentReleaseSource(activeJAR)
|
|
|
|
|
|
if sourceErr != nil {
|
|
|
|
|
|
t.Fatalf("resolve restart source: %v", sourceErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
restartJAR, sourceErr := updatepackage.OpenDirectNativeJAR(restartSource)
|
|
|
|
|
|
if sourceErr != nil {
|
|
|
|
|
|
t.Fatalf("open restart source: %v", sourceErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
restartReleasePath, sourceErr := updater.restartReleasePath(restartJAR)
|
|
|
|
|
|
if sourceErr != nil {
|
|
|
|
|
|
t.Fatalf("resolve restart release path: %v", sourceErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
pendingRestart, _, created, sourceErr := updater.createOrResume(ctx, updateInput{
|
|
|
|
|
|
IdempotencyKey: "backend:restart:test-resume",
|
|
|
|
|
|
InputType: inputTypeCurrentRelease,
|
|
|
|
|
|
SourcePath: restartJAR.Path,
|
|
|
|
|
|
SourceSHA256: restartJAR.SHA256,
|
|
|
|
|
|
ArtifactFileName: restartJAR.FileName,
|
|
|
|
|
|
ArtifactIdentity: restartJAR.Identity,
|
|
|
|
|
|
ReleasePath: restartReleasePath,
|
|
|
|
|
|
Materialize: restartJAR.CopyArtifact,
|
|
|
|
|
|
})
|
|
|
|
|
|
if sourceErr != nil || !created {
|
|
|
|
|
|
t.Fatalf("create interrupted restart transaction: record=%+v created=%t err=%v", pendingRestart, created, sourceErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
progress = nil
|
|
|
|
|
|
record, err = updater.Restart(ctx, report)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("restart native backend: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if record.ID == previousTransactionID || record.ID != pendingRestart.ID || record.State != transaction.StateCommitted {
|
|
|
|
|
|
t.Fatalf("unexpected resumed restart transaction: previous=%s pending=%s record=%+v", previousTransactionID, pendingRestart.ID, record)
|
|
|
|
|
|
}
|
|
|
|
|
|
if gateway.snapshot.ActivePort != 8081 || units.units["yms-backend@8081.service"].ActiveState != "active" || units.units["yms-backend@8080.service"].ActiveState != "failed" {
|
|
|
|
|
|
t.Fatalf("restart did not rotate native backend slots: gateway=%+v units=%+v", gateway.snapshot, units.units)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(progress) == 0 || progress[len(progress)-1].Message != "Native backend restart committed" {
|
|
|
|
|
|
t.Fatalf("missing committed restart progress: %+v", progress)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// serverConfiguration8081 测试用的 host Nginx 配置片段,其活跃后端端口为 8081。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
const serverConfiguration8081 = `http {
|
|
|
|
|
|
upstream yms-server {
|
|
|
|
|
|
# yms-update managed upstream begin
|
|
|
|
|
|
# server 10.11.1.117:8080 max_fails=1 fail_timeout=2s;
|
|
|
|
|
|
server 10.11.1.117:8081 max_fails=1 fail_timeout=2s;
|
|
|
|
|
|
# yms-update managed upstream end
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// memoryGateway 网关控制器的测试替身,在内存中保存 host Nginx 快照,
|
|
|
|
|
|
// 记录 Apply 调用次数,并支持在应用前注入失败以模拟切换故障。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
type memoryGateway struct {
|
2026-08-16 17:12:06 +08:00
|
|
|
|
snapshot hostnginx.Snapshot
|
|
|
|
|
|
applyCount int
|
|
|
|
|
|
beforeApply func(hostnginx.Snapshot) error
|
2026-08-16 01:27:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// Read 返回当前保存的网关快照副本。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func (g *memoryGateway) Read() (hostnginx.Snapshot, error) {
|
|
|
|
|
|
return hostnginx.Snapshot{Content: append([]byte(nil), g.snapshot.Content...), ActivePort: g.snapshot.ActivePort}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// Apply 在 beforeApply 钩子通过后,将给定快照保存为当前状态并累计应用次数。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func (g *memoryGateway) Apply(_ context.Context, snapshot hostnginx.Snapshot) error {
|
2026-08-16 17:12:06 +08:00
|
|
|
|
if g.beforeApply != nil {
|
|
|
|
|
|
if err := g.beforeApply(snapshot); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-16 01:27:30 +08:00
|
|
|
|
g.snapshot = hostnginx.Snapshot{Content: append([]byte(nil), snapshot.Content...), ActivePort: snapshot.ActivePort}
|
2026-08-16 17:12:06 +08:00
|
|
|
|
g.applyCount++
|
2026-08-16 01:27:30 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// updateUnitManager systemd 单元管理器的测试替身,在内存中维护单元状态,
|
|
|
|
|
|
// 并记录最近一次被停止的单元名称。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
type updateUnitManager struct {
|
|
|
|
|
|
units map[string]systemd.Unit
|
|
|
|
|
|
stopped string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// Inspect 返回指定名称的单元状态。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func (m *updateUnitManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
|
|
|
|
|
|
return m.units[name], nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// Start 将指定单元置为 active。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func (m *updateUnitManager) Start(_ context.Context, name string) error {
|
|
|
|
|
|
unit := m.units[name]
|
|
|
|
|
|
unit.ActiveState = "active"
|
|
|
|
|
|
m.units[name] = unit
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// Stop 将指定单元置为 failed 并记录其名称。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func (m *updateUnitManager) Stop(_ context.Context, name string) error {
|
|
|
|
|
|
unit := m.units[name]
|
|
|
|
|
|
unit.ActiveState = "failed"
|
|
|
|
|
|
m.units[name] = unit
|
|
|
|
|
|
m.stopped = name
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// preparingExecutor 原生后端执行器的测试替身:它把制品写入发布目录、
|
|
|
|
|
|
// 建立槽位软链接、启动目标单元并推进事务状态,模拟执行器在真实环境中的工作。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
type preparingExecutor struct {
|
|
|
|
|
|
store *transaction.Store
|
|
|
|
|
|
releaseDir string
|
|
|
|
|
|
units *updateUnitManager
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// Run 将请求中的制品落盘到发布目录,创建槽位软链接,启动目标单元,
|
|
|
|
|
|
// 并把事务依次推进到 Starting 状态,用于驱动后续切换与提交逻辑。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func (e *preparingExecutor) Run(ctx context.Context, transactionID string, request nativebackendexecutor.Request) error {
|
|
|
|
|
|
if err := os.MkdirAll(e.releaseDir, 0o750); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
content, err := os.ReadFile(request.ArtifactPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
installedPath := filepath.Join(e.releaseDir, request.ReleasePath)
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(installedPath), 0o750); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.WriteFile(installedPath, content, 0o640); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.Symlink(installedPath, request.SlotJarPath); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := e.units.Start(ctx, request.UnitName); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, state := range []transaction.State{
|
|
|
|
|
|
transaction.StateValidating,
|
|
|
|
|
|
transaction.StatePrepared,
|
|
|
|
|
|
transaction.StateStarting,
|
|
|
|
|
|
transaction.StateSwitching,
|
|
|
|
|
|
} {
|
|
|
|
|
|
if _, err := e.store.Transition(ctx, transactionID, state, "test transition"); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// writeNativeBackendPackage 生成一个包含 artifact-selection.json 清单和
|
|
|
|
|
|
// glory-soft-yms-test.jar 制品的 repack ZIP 包,返回其路径。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func writeNativeBackendPackage(t *testing.T, jar []byte) string {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
digest := sha256.Sum256(jar)
|
|
|
|
|
|
manifest := map[string]any{
|
|
|
|
|
|
"customerCode": "customer-01",
|
|
|
|
|
|
"customerDisplayName": "Customer 01",
|
|
|
|
|
|
"versionId": "V1.1.8",
|
|
|
|
|
|
"items": []string{"deploy-sync.sh"},
|
|
|
|
|
|
"backendArtifacts": []any{map[string]any{
|
|
|
|
|
|
"id": int64(42),
|
|
|
|
|
|
"versionCode": "V1.1.8",
|
|
|
|
|
|
"artifactKind": "BACKEND",
|
|
|
|
|
|
"type": "native",
|
|
|
|
|
|
"selectedType": "native",
|
|
|
|
|
|
"platform": nil,
|
|
|
|
|
|
"fileName": "glory-soft-yms-test.jar",
|
|
|
|
|
|
"filePath": "/archive/glory-soft-yms-test.jar",
|
|
|
|
|
|
"sha256": hex.EncodeToString(digest[:]),
|
|
|
|
|
|
"imageRef": nil,
|
|
|
|
|
|
}},
|
|
|
|
|
|
"frontendArtifacts": []any{},
|
|
|
|
|
|
"nodeSsrArtifacts": []any{},
|
|
|
|
|
|
"remark": nil,
|
|
|
|
|
|
}
|
|
|
|
|
|
manifestContent, err := json.Marshal(manifest)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("encode artifact selection: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
packagePath := filepath.Join(t.TempDir(), "package.zip")
|
|
|
|
|
|
file, err := os.Create(packagePath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create update package: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
writer := zip.NewWriter(file)
|
|
|
|
|
|
for name, content := range map[string][]byte{
|
|
|
|
|
|
"artifact-selection.json": manifestContent,
|
|
|
|
|
|
"glory-soft-yms-test.jar": jar,
|
|
|
|
|
|
} {
|
|
|
|
|
|
entry, err := writer.Create(name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create update package entry: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := entry.Write(content); err != nil {
|
|
|
|
|
|
t.Fatalf("write update package entry: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := writer.Close(); err != nil {
|
|
|
|
|
|
t.Fatalf("close update package writer: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := file.Close(); err != nil {
|
|
|
|
|
|
t.Fatalf("close update package: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return packagePath
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// writeDirectBackendJAR 生成一个包含 BOOT-INF/classes/application.properties 的直接 native JAR,
|
|
|
|
|
|
// 返回其路径,供直接安装更新路径使用。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func writeDirectBackendJAR(t *testing.T, content []byte) string {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
jarPath := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
|
|
|
|
|
|
file, err := os.Create(jarPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create direct backend JAR: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
writer := zip.NewWriter(file)
|
|
|
|
|
|
entry, err := writer.Create("BOOT-INF/classes/application.properties")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create direct backend JAR entry: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := entry.Write(content); err != nil {
|
|
|
|
|
|
t.Fatalf("write direct backend JAR entry: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := writer.Close(); err != nil {
|
|
|
|
|
|
t.Fatalf("close direct backend JAR writer: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := file.Close(); err != nil {
|
|
|
|
|
|
t.Fatalf("close direct backend JAR: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return jarPath
|
|
|
|
|
|
}
|