feat(job-array-one-based-index): xcecli submit shows the array (task 8.3)

Submitting an array prints the server's message with the array ID in
written form (no [k]) and the element count, e.g.
'Job <0-0-...> is submitted to queue <normal> (3 elements).'; ordinary
submits print exactly as before. The client model mirrors array_size.
Submit help documents name[1-N] and name[1], numbering from 1 and the
API-server limit (default 1000, at most 32767).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-01 12:58:36 +00:00
parent 58ca7abb36
commit d3b12159a4
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
4 changed files with 141 additions and 11 deletions

View file

@ -198,7 +198,7 @@
- _Requirements: 5.1, 5.2, 5.16, 8.2, 8.3, 9.2_
- _Depends: 8.1_
- [ ] 8.3 Show the array in submit output and help
- [x] 8.3 Show the array in submit output and help
- Add the element count to the client's submit response model; print the server's submit message with the array identity formatted (the count comes from the message); describe `name[1-N]`, `name[1]`, numbering from 1 and the limit (default 1000, at most 32767) in submit help
- Done when a submit-output test shows `Job <array-id> is submitted ... (3 elements)` with the formatted array identity and the help text lists the supported forms
- _Requirements: 3.1, 3.2, 9.1_

View file

@ -489,19 +489,11 @@ func (js *JobSubmitter) RunJobsSubmit() error {
return err
}
jobReq.TransferFiles = transferFile
jobResp, err := apiClient.SubmitJob("Bearer "+serverInfo.Token, jobReq)
logger.Debug("jobResp", zap.Any("jobResp", jobResp), zap.Error(err))
logger.Info("jobResp", zap.Any("jobResp", jobResp), zap.Error(err))
jobResp, err := submitAndReport(apiClient, serverInfo.Token, jobReq)
if err != nil {
return err
}
msg := strings.Replace(jobResp.Data.Message, fmt.Sprintf("<%d>", jobResp.Data.JobID), fmt.Sprintf("<%s>", format.FormatJobID(jobResp.Data.JobID)), 1)
fmt.Println(msg)
logger.Debug("job submitted successfully", zap.String("message", jobResp.Data.Message))
logger.Info("job submitted successfully", zap.String("message", jobResp.Data.Message))
if js.interactive {
fmt.Println("<<waiting for dispatch ...>>")
url, err := js.waitForInteractiveURL(apiClient, serverInfo.Token, fmt.Sprintf("%d", jobResp.Data.JobID))
@ -520,6 +512,26 @@ func (js *JobSubmitter) RunJobsSubmit() error {
return nil
}
// submitAndReport submits jobReq and prints the server's message with the
// returned identity in written form. For an array the identity is the array
// ID (no element number) and the message already carries "(n elements)"
// (3.1, 3.2), so nothing is appended here.
func submitAndReport(apiClient *request.APIClient, token string, jobReq *models.JobSubmitRequest) (*models.JobSubmitResponse, error) {
jobResp, err := apiClient.SubmitJob("Bearer "+token, jobReq)
logger.Debug("jobResp", zap.Any("jobResp", jobResp), zap.Error(err))
logger.Info("jobResp", zap.Any("jobResp", jobResp), zap.Error(err))
if err != nil {
return nil, err
}
msg := strings.Replace(jobResp.Data.Message, fmt.Sprintf("<%d>", jobResp.Data.JobID), fmt.Sprintf("<%s>", format.FormatJobID(jobResp.Data.JobID)), 1)
fmt.Println(msg)
logger.Debug("job submitted successfully", zap.String("message", jobResp.Data.Message))
logger.Info("job submitted successfully", zap.String("message", jobResp.Data.Message))
return jobResp, nil
}
func (js *JobSubmitter) checkJobStatus(apiClient *request.APIClient, token string, jobID string) (string, error) {
var resp models.JobsResponse
err := apiClient.GetRequestFromAPIServer(
@ -650,7 +662,17 @@ Example:
xce jobs submit --configfile job.xce # submit job using parameters from a configuration file
xce jobs submit -m "host1 host2" sleep 10 # submit job into host1 host2
xce jobs submit -I --tty sleep 10 # submit interactive job with tty
xce jobs submit -I --xjob sleep 10 # submit interactive job with xjob`,
xce jobs submit -I --xjob sleep 10 # submit interactive job with xjob
xce jobs submit -J 'name[1-N]' sleep 10 # submit a job array of N elements named "name"
xce jobs submit -J 'name[1]' sleep 10 # submit a job array of one element
Job arrays:
-J 'name[1-N]' submits N elements, numbered 1 through N; -J 'name[1]'
submits a one-element array. Element numbers start at 1. The maximum
array size is set by the API server's configuration (default 1000,
never above 32767). Submit prints the array identity, e.g.
Job <array-id> is submitted to queue <normal> (N elements).
Refer to one element as <array-id>[k]. Interactive jobs cannot be arrays.`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {

View file

@ -1,8 +1,20 @@
package jobs
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"go.uber.org/zap"
"xcecli/common/format"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
)
// bug-1676: BuildJobRequest asserts our own hostname as FromHost rather than
@ -25,3 +37,96 @@ func TestBuildJobRequest_SetsFromHostToLocalHostname(t *testing.T) {
t.Errorf("FromHost = %q, want local hostname %q", req.FromHost, wantHost)
}
}
// submitServer answers every submit with the given response data.
func submitServer(t *testing.T, data models.JobSubmitData) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(models.JobSubmitResponse{Code: 0, Msg: "ok", Data: data})
}))
t.Cleanup(srv.Close)
// The submit path logs through config.Log, which only main initialises.
if config.Log == nil {
config.Log = zap.NewNop()
t.Cleanup(func() { config.Log = nil })
}
return srv
}
// An accepted array submission prints the array identity in written form,
// without an element number, and the server's element count (3.1, 3.2); the
// count is mirrored in the client model for scripted use.
func TestSubmitOutput_ArrayIdentityAndCount(t *testing.T) {
srv := submitServer(t, models.JobSubmitData{
JobID: int64(testArrayKey),
Message: fmt.Sprintf("Job <%d> is submitted to queue <normal> (3 elements).", testArrayKey),
ArraySize: 3,
})
js := NewJobSubmitter(nil)
js.command = "sleep 1"
req, err := js.BuildJobRequest()
if err != nil {
t.Fatalf("BuildJobRequest: %v", err)
}
var resp *models.JobSubmitResponse
out, err := captureStdout(t, func() error {
var e error
resp, e = submitAndReport(request.NewAPIClient(srv.URL), "tok", req)
return e
})
if err != nil {
t.Fatalf("submitAndReport: %v", err)
}
want := "Job <" + testArrayWritten + "> is submitted to queue <normal> (3 elements).\n"
if out != want {
t.Fatalf("output = %q, want %q", out, want)
}
if resp.Data.ArraySize != 3 {
t.Errorf("ArraySize = %d, want 3", resp.Data.ArraySize)
}
}
// An ordinary submission prints exactly as before (3.3): no count added.
func TestSubmitOutput_OrdinaryUnchanged(t *testing.T) {
id := format.MakeJobID(0, 0, 1700000000, 0, 7)
srv := submitServer(t, models.JobSubmitData{
JobID: int64(id),
Message: fmt.Sprintf("Job <%d> is submitted to queue <normal>.", id),
})
js := NewJobSubmitter(nil)
js.command = "sleep 1"
req, err := js.BuildJobRequest()
if err != nil {
t.Fatalf("BuildJobRequest: %v", err)
}
var resp *models.JobSubmitResponse
out, err := captureStdout(t, func() error {
var e error
resp, e = submitAndReport(request.NewAPIClient(srv.URL), "tok", req)
return e
})
if err != nil {
t.Fatalf("submitAndReport: %v", err)
}
want := "Job <0-0-1700000000-7> is submitted to queue <normal>.\n"
if out != want {
t.Fatalf("output = %q, want %q", out, want)
}
if resp.Data.ArraySize != 0 {
t.Errorf("ArraySize = %d, want 0", resp.Data.ArraySize)
}
}
// Submit help describes the array forms, numbering from 1 and the limit (9.1).
func TestSubmitHelpShowsArrayForms(t *testing.T) {
long := NewSubmitJobsCmd(nil).Long
for _, s := range []string{
"-J 'name[1-N]'", "-J 'name[1]'", "start at 1",
"set by the API server", "default 1000", "32767",
} {
if !strings.Contains(long, s) {
t.Errorf("submit help lacks %q", s)
}
}
}

View file

@ -365,6 +365,9 @@ type JobSubmitRequest struct {
type JobSubmitData struct {
JobID int64 `json:"jobid"`
Message string `json:"message"`
// ArraySize is the number of elements of a submitted array (3.2); zero
// for an ordinary job. The printed count comes from Message.
ArraySize int `json:"array_size,omitempty"`
}
// JobSubmitResponse define the data structure in the job submit response