feat(job-array-one-based-index): apiserver backend ID helpers and array ID wrapper (task 5.1)
GenerateArrayJobID wraps libxce idgen_array (first element, index 1; one-element arrays included) with a non-cgo stub; pure helpers ParseHumanJobIDStrict (written identity with exact field-width checks, uint64 overflow reported as a width error), IsArrayCapableKey and MakeElementID (bit-identical to libxce jobsel_element_id). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47b9938120
commit
b88e352616
7 changed files with 260 additions and 3 deletions
|
|
@ -94,7 +94,7 @@
|
||||||
- _Depends: 1.1, 1.2_
|
- _Depends: 1.1, 1.2_
|
||||||
|
|
||||||
- [ ] 5. API server foundation
|
- [ ] 5. API server foundation
|
||||||
- [ ] 5.1 Add backend ID helpers and the array ID wrapper
|
- [x] 5.1 Add backend ID helpers and the array ID wrapper
|
||||||
- Wrap the libxce array allocator for the API server, with a non-cgo stub that returns an error
|
- Wrap the libxce array allocator for the API server, with a non-cgo stub that returns an error
|
||||||
- Add strict written-identity parsing with field-width checks, an array-capable key test and element-ID composition as pure helpers
|
- Add strict written-identity parsing with field-width checks, an array-capable key test and element-ID composition as pure helpers
|
||||||
- Done when the backend unit tests pass for the strict parser (width rejections), the array-capable test and element composition, and both the cgo and non-cgo builds compile
|
- Done when the backend unit tests pass for the strict parser (width rejections), the array-capable test and element composition, and both the cgo and non-cgo builds compile
|
||||||
|
|
@ -243,3 +243,4 @@
|
||||||
- 2.2: two extra XCE_TESTING seams exist (`jobsubmit_test_release_hook`, `jobsubmit_inflight_try_add_for_test`); the controller test fixture has no WAL, so async commit-failure runs synchronously inside handle_jobsubmit — reuse it in 2.3. Stale comment at `src/controller/testsuite/test_commit_barrier_reply.c:1458` (claims release via jobsubmit_async_ctx_release) — fix in 2.3.
|
- 2.2: two extra XCE_TESTING seams exist (`jobsubmit_test_release_hook`, `jobsubmit_inflight_try_add_for_test`); the controller test fixture has no WAL, so async commit-failure runs synchronously inside handle_jobsubmit — reuse it in 2.3. Stale comment at `src/controller/testsuite/test_commit_barrier_reply.c:1458` (claims release via jobsubmit_async_ctx_release) — fix in 2.3.
|
||||||
- 4.1: after `jobsub -a N`, `$jobid` is the array identity, so `expect-job $jobid` currently reports not found; 4.3 must make it behave per 4.7/4.9 (not-found hint naming `<array-id>[k]`, or exact-ID match). Nit for 4.3: the `refused` early return in `cmd_jobsub` sits between the bug-786 ack comment and the code it describes — move it above the comment.
|
- 4.1: after `jobsub -a N`, `$jobid` is the array identity, so `expect-job $jobid` currently reports not found; 4.3 must make it behave per 4.7/4.9 (not-found hint naming `<array-id>[k]`, or exact-ID match). Nit for 4.3: the `refused` early return in `cmd_jobsub` sits between the bug-786 ack comment and the code it describes — move it above the comment.
|
||||||
- 4.3: the controller acks `setpend` (`do_set_preason`, job.c ~3869-3880) and `jobrun` (`handle_jobdispatch`, job.c ~2246-2258) with status 0 before doing the work, so those two harness commands can never report not-found; 4.7/5.13 for them depend on a controller change outside this spec's boundary. `jobinfo`'s not-found is `rc == 0` with no row (`do_reply_joblist`), not -ENOENT.
|
- 4.3: the controller acks `setpend` (`do_set_preason`, job.c ~3869-3880) and `jobrun` (`handle_jobdispatch`, job.c ~2246-2258) with status 0 before doing the work, so those two harness commands can never report not-found; 4.7/5.13 for them depend on a controller change outside this spec's boundary. `jobinfo`'s not-found is `rc == 0` with no row (`do_reply_joblist`), not -ENOENT.
|
||||||
|
- 5.1: Go helpers: `ParseHumanJobIDStrict(s) (uint64, error)` returns sentinel `ErrJobIDSyntax` / `ErrJobIDWidth` (callers trim and split `[...]` first; no whitespace accepted); `MakeElementID(key uint64, k int) uint64`; `IsArrayCapableKey(key)`; `GenerateArrayJobID(c, a, n)` (non-cgo stub returns errNoCGO).
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,25 @@ func GenerateJobID(clusterID, apiserverID int, arraySize int) (uint64, error) {
|
||||||
return uint64(jobID), nil
|
return uint64(jobID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateArrayJobID calls the C idgen_array() to allocate a new array of n
|
||||||
|
// elements (1 <= n <= 32767, numbered from 1) and returns the first element's
|
||||||
|
// ID (tag TAG_JOB_ARRAY, index 1); ExtractArrayBaseID of it is the array ID.
|
||||||
|
// n == 1 is a one-element array, not an ordinary job (job-array-one-based-index
|
||||||
|
// 1.1, 1.2). Errors on a bad n or when the sequence block would overflow.
|
||||||
|
func GenerateArrayJobID(clusterID, apiserverID int, n int) (uint64, error) {
|
||||||
|
if n < 1 {
|
||||||
|
return 0, fmt.Errorf("idgen_array: invalid array size %d", n)
|
||||||
|
}
|
||||||
|
idCtxOnce.Do(initIDGen)
|
||||||
|
var first C.uint64_t
|
||||||
|
rc := C.idgen_array(&gIDCtx, C.size_t(clusterID), C.size_t(apiserverID),
|
||||||
|
C.size_t(n), &first)
|
||||||
|
if rc < 0 {
|
||||||
|
return 0, fmt.Errorf("idgen_array failed: %d", int(rc))
|
||||||
|
}
|
||||||
|
return uint64(first), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup releases C resources. Call at process shutdown.
|
// Cleanup releases C resources. Call at process shutdown.
|
||||||
func Cleanup() {
|
func Cleanup() {
|
||||||
C.idgen_ctx_fini(&gIDCtx)
|
C.idgen_ctx_fini(&gIDCtx)
|
||||||
|
|
|
||||||
|
|
@ -303,3 +303,33 @@ func TestValidateResreqSyntax(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// job-array-one-based-index 1.1/1.2: GenerateArrayJobID wraps libxce
|
||||||
|
// idgen_array and returns the first element (index 1) of an array whose
|
||||||
|
// identity is array-capable, for n = 1 (one-element array) and n > 1.
|
||||||
|
func TestGenerateArrayJobID_FirstElementIsIndexOne(t *testing.T) {
|
||||||
|
for _, n := range []int{1, 3, 32767} {
|
||||||
|
first, err := GenerateArrayJobID(1, 2, n)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateArrayJobID(1, 2, %d): err=%v", n, err)
|
||||||
|
}
|
||||||
|
if !IsArrayJob(first) || ExtractArrayIdx(first) != 1 {
|
||||||
|
t.Errorf("n=%d: first=%#x, want tagged element with index 1", n, first)
|
||||||
|
}
|
||||||
|
key := ExtractArrayBaseID(first)
|
||||||
|
if !IsArrayCapableKey(key) || MakeElementID(key, 1) != first {
|
||||||
|
t.Errorf("n=%d: array identity %#x not array-capable or not the first element's", n, key)
|
||||||
|
}
|
||||||
|
if c, a := extractBits(first, clusterIDShift, nClusterIDBits), extractBits(first, apiserverIDShift, nApiserverIDBits); c != 1 || a != 2 {
|
||||||
|
t.Errorf("n=%d: cluster/apiserver = %d/%d, want 1/2", n, c, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateArrayJobID_RejectsBadSize(t *testing.T) {
|
||||||
|
for _, n := range []int{0, -1, 32768} {
|
||||||
|
if id, err := GenerateArrayJobID(1, 2, n); err == nil {
|
||||||
|
t.Errorf("GenerateArrayJobID(1, 2, %d): got (%#x, nil), want an error", n, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ package backend
|
||||||
import (
|
import (
|
||||||
"apiserver/models"
|
"apiserver/models"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
"fmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// JobID bit layout constants — mirrors src/include/config.h.
|
// JobID bit layout constants — mirrors src/include/config.h.
|
||||||
|
|
@ -132,6 +133,66 @@ func ParseHumanJobID(s string) (uint64, bool) {
|
||||||
sequence, true
|
sequence, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Errors returned by ParseHumanJobIDStrict. Callers map them to their own
|
||||||
|
// messages: ErrJobIDSyntax is the written-form syntax rule, ErrJobIDWidth the
|
||||||
|
// field-width rule (job-array-one-based-index 5.14).
|
||||||
|
var (
|
||||||
|
ErrJobIDSyntax = errors.New("job ID is not of the form cluster-apiserver-timestamp-sequence")
|
||||||
|
ErrJobIDWidth = errors.New("job ID field out of range")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseHumanJobIDStrict parses a written job identity "c-a-ts-seq" (decimal
|
||||||
|
// digits only, leading zeros allowed, no sign, no whitespace, no "[...]")
|
||||||
|
// into its packed, untagged ID. Unlike ParseHumanJobID it checks every field
|
||||||
|
// against its bit width -- cluster <= 63, apiserver <= 15, timestamp < 2^32,
|
||||||
|
// sequence < 2^20 -- so an oversized field is rejected (ErrJobIDWidth)
|
||||||
|
// instead of spilling into a neighbouring field. A field too large even for
|
||||||
|
// uint64 is also ErrJobIDWidth. Anything not of that shape is ErrJobIDSyntax.
|
||||||
|
func ParseHumanJobIDStrict(s string) (uint64, error) {
|
||||||
|
m := humanJobIDPattern.FindStringSubmatch(s)
|
||||||
|
if m == nil {
|
||||||
|
return 0, ErrJobIDSyntax
|
||||||
|
}
|
||||||
|
widths := [4]int{nClusterIDBits, nApiserverIDBits, nTimestampBits, nSequenceBits}
|
||||||
|
var f [4]uint64
|
||||||
|
for i, w := range widths {
|
||||||
|
v, err := strconv.ParseUint(m[i+1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, strconv.ErrRange) {
|
||||||
|
return 0, ErrJobIDWidth
|
||||||
|
}
|
||||||
|
return 0, ErrJobIDSyntax
|
||||||
|
}
|
||||||
|
if v >= uint64(1)<<w {
|
||||||
|
return 0, ErrJobIDWidth
|
||||||
|
}
|
||||||
|
f[i] = v
|
||||||
|
}
|
||||||
|
return (f[0] << clusterIDShift) |
|
||||||
|
(f[1] << apiserverIDShift) |
|
||||||
|
(f[2] << timestampShift) |
|
||||||
|
f[3], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrayIdxMask covers the 15-bit array index field (bits 0-14).
|
||||||
|
const arrayIdxMask = uint64(1)<<nArrayIdxBits - 1
|
||||||
|
|
||||||
|
// IsArrayCapableKey reports whether key can name an array (an "<array-id>"
|
||||||
|
// in "<array-id>[k]"): tag 0 and the low 15 (index) bits clear, i.e. the
|
||||||
|
// sequence is a multiple of 32768 (job-array-one-based-index 5.8).
|
||||||
|
func IsArrayCapableKey(key uint64) bool {
|
||||||
|
return extractIDTag(key) == 0 && key&arrayIdxMask == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeElementID composes the ID of element k of the array identified by key:
|
||||||
|
// key's cluster, apiserver, timestamp and sequence-high bits, tag
|
||||||
|
// TAG_JOB_ARRAY and index k (masked to 15 bits). key's own tag and index bits
|
||||||
|
// are ignored. Mirrors jobsel_element_id() in src/lib/utils.c.
|
||||||
|
func MakeElementID(key uint64, k int) uint64 {
|
||||||
|
base := key &^ (uint64(1<<nTagBits-1)<<tagShift | arrayIdxMask)
|
||||||
|
return uint64(tagJobArray)<<tagShift | base | uint64(k)&arrayIdxMask
|
||||||
|
}
|
||||||
|
|
||||||
// WorkerDefaultOutputPath returns the output file path the worker uses by default
|
// WorkerDefaultOutputPath returns the output file path the worker uses by default
|
||||||
// for the given job ID and task index, matching uv_proc.c make_job_output_path().
|
// for the given job ID and task index, matching uv_proc.c make_job_output_path().
|
||||||
func WorkerDefaultOutputPath(uniqueJobID int64, taskIdx int) string {
|
func WorkerDefaultOutputPath(uniqueJobID int64, taskIdx int) string {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package backend
|
||||||
import (
|
import (
|
||||||
"apiserver/models"
|
"apiserver/models"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
@ -99,6 +100,137 @@ func TestParseHumanJobID_RejectsGarbage(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// job-array-one-based-index 5.14: the strict written-identity parser
|
||||||
|
// accepts c-a-ts-seq only when every field fits its bit width.
|
||||||
|
func TestParseHumanJobIDStrict_AcceptsMaxWidths(t *testing.T) {
|
||||||
|
s := fmt.Sprintf("63-15-%d-%d", uint64(1)<<32-1, uint64(1)<<20-1)
|
||||||
|
got, err := ParseHumanJobIDStrict(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseHumanJobIDStrict(%q): err=%v, want nil", s, err)
|
||||||
|
}
|
||||||
|
if want := makeJobID(63, 15, uint64(1)<<32-1, 0, uint64(1)<<20-1); got != want {
|
||||||
|
t.Errorf("ParseHumanJobIDStrict(%q): got %d, want %d", s, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_RoundTripsWithFormatJobID(t *testing.T) {
|
||||||
|
packed := makeJobID(1, 2, 1788768547, 0, 3<<sequenceHighShift)
|
||||||
|
s := FormatJobID(int64(packed))
|
||||||
|
got, err := ParseHumanJobIDStrict(s)
|
||||||
|
if err != nil || got != packed {
|
||||||
|
t.Errorf("ParseHumanJobIDStrict(%q): got (%d, %v), want (%d, nil)", s, got, err, packed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_AcceptsLeadingZeros(t *testing.T) {
|
||||||
|
got, err := ParseHumanJobIDStrict("01-002-0003-00042")
|
||||||
|
if want := makeJobID(1, 2, 3, 0, 42); err != nil || got != want {
|
||||||
|
t.Errorf("ParseHumanJobIDStrict(leading zeros): got (%d, %v), want (%d, nil)", got, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_RejectsClusterTooWide(t *testing.T) {
|
||||||
|
if _, err := ParseHumanJobIDStrict("64-0-1-0"); !errors.Is(err, ErrJobIDWidth) {
|
||||||
|
t.Errorf("cluster 64: err=%v, want ErrJobIDWidth", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_RejectsApiserverTooWide(t *testing.T) {
|
||||||
|
if _, err := ParseHumanJobIDStrict("1-16-1-0"); !errors.Is(err, ErrJobIDWidth) {
|
||||||
|
t.Errorf("apiserver 16: err=%v, want ErrJobIDWidth", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_RejectsTimestampTooWide(t *testing.T) {
|
||||||
|
s := fmt.Sprintf("1-0-%d-0", uint64(1)<<32)
|
||||||
|
if _, err := ParseHumanJobIDStrict(s); !errors.Is(err, ErrJobIDWidth) {
|
||||||
|
t.Errorf("timestamp 2^32: err=%v, want ErrJobIDWidth", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_RejectsSequenceTooWide(t *testing.T) {
|
||||||
|
s := fmt.Sprintf("1-0-1-%d", uint64(1)<<20)
|
||||||
|
if _, err := ParseHumanJobIDStrict(s); !errors.Is(err, ErrJobIDWidth) {
|
||||||
|
t.Errorf("sequence 2^20: err=%v, want ErrJobIDWidth", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A field too large for uint64 must be a width error, not a silent wrap
|
||||||
|
// (the ParseUint error is not discarded).
|
||||||
|
func TestParseHumanJobIDStrict_RejectsUint64Overflow(t *testing.T) {
|
||||||
|
if _, err := ParseHumanJobIDStrict("1-0-99999999999999999999999-0"); !errors.Is(err, ErrJobIDWidth) {
|
||||||
|
t.Errorf("timestamp > uint64: err=%v, want ErrJobIDWidth", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHumanJobIDStrict_RejectsBadSyntax(t *testing.T) {
|
||||||
|
for _, s := range []string{"", "73933253805867008", "1-2-3", "1-2-3-4-5",
|
||||||
|
"1-2-3-4[1]", " 1-2-3-4", "1-2-3-4 ", "+1-2-3-4", "1--2-3", "a-b-c-d"} {
|
||||||
|
if _, err := ParseHumanJobIDStrict(s); !errors.Is(err, ErrJobIDSyntax) {
|
||||||
|
t.Errorf("ParseHumanJobIDStrict(%q): err=%v, want ErrJobIDSyntax", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.8: an array identity has tag 0 and its low 15 bits clear.
|
||||||
|
func TestIsArrayCapableKey_AlignedUntagged(t *testing.T) {
|
||||||
|
if !IsArrayCapableKey(makeJobID(1, 0, 100, 0, 5<<sequenceHighShift)) {
|
||||||
|
t.Error("aligned untagged key: got false, want true")
|
||||||
|
}
|
||||||
|
if !IsArrayCapableKey(makeJobID(1, 0, 100, 0, 0)) {
|
||||||
|
t.Error("sequence 0 key: got false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsArrayCapableKey_RejectsMisaligned(t *testing.T) {
|
||||||
|
if IsArrayCapableKey(makeJobID(1, 0, 100, 0, 5<<sequenceHighShift|1)) {
|
||||||
|
t.Error("key with low bit set: got true, want false")
|
||||||
|
}
|
||||||
|
if IsArrayCapableKey(makeJobID(1, 0, 100, 0, 1<<14)) {
|
||||||
|
t.Error("key with bit 14 set: got true, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsArrayCapableKey_RejectsTagged(t *testing.T) {
|
||||||
|
if IsArrayCapableKey(makeJobID(1, 0, 100, tagJobArray, 5<<sequenceHighShift)) {
|
||||||
|
t.Error("tagged key: got true, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeElementID_ComposesTaggedElement(t *testing.T) {
|
||||||
|
key := makeJobID(3, 2, 1788768547, 0, 7<<sequenceHighShift)
|
||||||
|
got := MakeElementID(key, 5)
|
||||||
|
if want := makeJobID(3, 2, 1788768547, tagJobArray, 7<<sequenceHighShift|5); got != want {
|
||||||
|
t.Fatalf("MakeElementID: got %#x, want %#x", got, want)
|
||||||
|
}
|
||||||
|
if !IsArrayJob(got) || ExtractArrayIdx(got) != 5 || ExtractArrayBaseID(got) != key {
|
||||||
|
t.Errorf("MakeElementID(%#x, 5) = %#x does not round-trip (tag/idx/base)", key, got)
|
||||||
|
}
|
||||||
|
if s, want := FormatJobID(int64(got)), FormatJobID(int64(key))+"[5]"; s != want {
|
||||||
|
t.Errorf("FormatJobID(element): got %q, want %q", s, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeElementID_MaxIndexAndZero(t *testing.T) {
|
||||||
|
key := makeJobID(1, 0, 100, 0, 1<<sequenceHighShift)
|
||||||
|
if got := ExtractArrayIdx(MakeElementID(key, 32767)); got != 32767 {
|
||||||
|
t.Errorf("element 32767: idx=%d, want 32767", got)
|
||||||
|
}
|
||||||
|
el0 := MakeElementID(key, 0)
|
||||||
|
if !IsArrayJob(el0) || ExtractArrayIdx(el0) != 0 {
|
||||||
|
t.Errorf("element 0: got %#x, want tagged with idx 0", el0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors jobsel_element_id: the key's own tag/index bits never leak in.
|
||||||
|
func TestMakeElementID_IgnoresKeyIndexAndTagBits(t *testing.T) {
|
||||||
|
clean := makeJobID(1, 0, 100, 0, 2<<sequenceHighShift)
|
||||||
|
dirty := clean | uint64(tagJobArray)<<tagShift | 9
|
||||||
|
if got, want := MakeElementID(dirty, 4), MakeElementID(clean, 4); got != want {
|
||||||
|
t.Errorf("MakeElementID(dirty key): got %#x, want %#x", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRawJobToModel_BasicFields(t *testing.T) {
|
func TestRawJobToModel_BasicFields(t *testing.T) {
|
||||||
r := rawJobData{
|
r := rawJobData{
|
||||||
jobID: 123,
|
jobID: 123,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ func GenerateJobID(clusterID, apiserverID int, arraySize int) (uint64, error) {
|
||||||
return 0, errNoCGO
|
return 0, errNoCGO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateArrayJobID stub — real implementation in clib.go (requires CGO).
|
||||||
|
func GenerateArrayJobID(clusterID, apiserverID int, n int) (uint64, error) {
|
||||||
|
return 0, errNoCGO
|
||||||
|
}
|
||||||
|
|
||||||
// SubmitJob stub — real implementation in clib.go (requires CGO).
|
// SubmitJob stub — real implementation in clib.go (requires CGO).
|
||||||
func SubmitJob(job *models.JobInfo, arraySize int) error { return errNoCGO }
|
func SubmitJob(job *models.JobInfo, arraySize int) error { return errNoCGO }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,13 @@ func TestRegisterToBackend_StubWithPeers(t *testing.T) {
|
||||||
if err != errNoCGO {
|
if err != errNoCGO {
|
||||||
t.Errorf("expected errNoCGO with peers configured, got %v", err)
|
t.Errorf("expected errNoCGO with peers configured, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGenerateArrayJobID_StubErrNoCGO verifies the no-CGO stub for
|
||||||
|
// GenerateArrayJobID returns errNoCGO and no ID (job-array-one-based-index 5.1).
|
||||||
|
func TestGenerateArrayJobID_StubErrNoCGO(t *testing.T) {
|
||||||
|
id, err := GenerateArrayJobID(1, 1, 3)
|
||||||
|
if err != errNoCGO || id != 0 {
|
||||||
|
t.Errorf("GenerateArrayJobID stub: got (%d, %v), want (0, errNoCGO)", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue