package executor import ( "context" "runtime" "strings" "testing" "time" ) // TestRun_BinaryEmpty covers the ErrBinaryEmpty path. The agent-facing // behavior is "list the cluster, fix the binary field" — we just guard the // error here. func TestRun_BinaryEmpty(t *testing.T) { _, err := Run(context.Background(), SparkSubmitOpts{Binary: "", Args: []string{"--help"}}) if err == nil || !strings.Contains(err.Error(), "empty") { t.Fatalf("want empty-binary error, got %v", err) } } // TestRun_FakeBinary uses /bin/echo as a stand-in for spark-submit. We // verify slice-form arg passing (no shell) by including an arg that would // be catastrophic if interpreted by a shell. func TestRun_FakeBinary(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("uses unix binary") } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() res, err := Run(ctx, SparkSubmitOpts{ Binary: "/bin/echo", Args: []string{"--master", "yarn; rm -rf /tmp/this-should-not-exist", "Submitted application application_12345_0001"}, Timeout: 3 * time.Second, }) if err != nil { t.Fatalf("Run: %v", err) } if res.ExitCode != 0 { t.Fatalf("ExitCode: got %d, want 0", res.ExitCode) } // The arg must be passed through verbatim, not interpreted by a shell. if !strings.Contains(res.StdoutTail, "yarn; rm -rf /tmp/this-should-not-exist") { t.Errorf("stdout missing literal arg, got: %q", res.StdoutTail) } if res.AppID != "application_12345_0001" { t.Errorf("AppID = %q, want application_12345_0001", res.AppID) } } // TestRun_NonZeroExit captures the exit code when the binary returns !=0. func TestRun_NonZeroExit(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("uses unix binary") } res, err := Run(context.Background(), SparkSubmitOpts{ Binary: "/bin/sh", Args: []string{"-c", "exit 7"}, Timeout: 3 * time.Second, }) if err == nil { t.Fatal("want non-nil error from non-zero exit") } if res.ExitCode != 7 { t.Errorf("ExitCode: got %d, want 7", res.ExitCode) } } // TestSubmittedAppPattern locks down the regex so a YARN output tweak doesn't // silently break app_id extraction. func TestSubmittedAppPattern(t *testing.T) { cases := []struct { in, want string }{ {"Submitted application application_12345_0001", "application_12345_0001"}, {"Submitted application application_999_42 extra", "application_999_42"}, {"no app id here", ""}, {"", ""}, {"Submitted application app_1_2 Submitted application app_3_4", "app_1_2"}, // first match } for _, tc := range cases { m := submittedAppPattern.FindStringSubmatch(tc.in) var got string if m != nil { got = m[1] } if got != tc.want { t.Errorf("submittedAppPattern(%q) = %q, want %q", tc.in, got, tc.want) } } } // TestCapBuf ensures the tail-capping behavior: writing more than `cap` // bytes keeps the most recent `cap` bytes. func TestCapBuf(t *testing.T) { b := &capBuf{cap: 10} // Write 20 bytes total in two chunks. if _, err := b.Write([]byte("0123456789")); err != nil { t.Fatal(err) } if _, err := b.Write([]byte("ABCDEFGHIJ")); err != nil { t.Fatal(err) } // Tail should be the last 10 bytes: "ABCDEFGHIJ". if got := b.String(); got != "ABCDEFGHIJ" { t.Errorf("capBuf tail: got %q, want %q", got, "ABCDEFGHIJ") } }