webui: add "6. Scenario" page — run scriptable test scenarios from removable media through the normal task queue
Exposes the scenario engine (platform.System.RunScenario, added earlier) in the web UI instead of only the `bee run` CLI: a new nav item lists every scenarios/*.json found on mounted removable media (GET /api/scenario/list) and runs one with a click (POST /api/scenario/run), enqueued as a normal Task with target "scenario" — progress/logs live in Tasks like any other SAT pack, no separate live-output UI needed. - app.go: satRunner gains RunScenario, exportManager gains ListScenarioFilesOnRemovableMedia/ReadScenarioFromRemovableMedia — both already implemented on platform.System, just newly exposed through App. - webui/tasks.go: taskParams.ScenarioName; runTask's "scenario" case reads the file from removable media, parses it, and runs it. - webui/page_scenario.go: the page itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
20cd317c87
commit
bcf02e0515
@@ -73,6 +73,8 @@ type serviceManager interface {
|
|||||||
type exportManager interface {
|
type exportManager interface {
|
||||||
ListRemovableTargets() ([]platform.RemovableTarget, error)
|
ListRemovableTargets() ([]platform.RemovableTarget, error)
|
||||||
ExportFileToTarget(src string, target platform.RemovableTarget) (string, error)
|
ExportFileToTarget(src string, target platform.RemovableTarget) (string, error)
|
||||||
|
ListScenarioFilesOnRemovableMedia() ([]platform.ScenarioFileOnRemovableMedia, error)
|
||||||
|
ReadScenarioFromRemovableMedia(name string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type toolManager interface {
|
type toolManager interface {
|
||||||
@@ -154,6 +156,7 @@ type satRunner interface {
|
|||||||
RunFanStressTest(ctx context.Context, baseDir string, opts platform.FanStressOptions) (string, error)
|
RunFanStressTest(ctx context.Context, baseDir string, opts platform.FanStressOptions) (string, error)
|
||||||
RunPlatformStress(ctx context.Context, baseDir string, opts platform.PlatformStressOptions, logFunc func(string)) (string, error)
|
RunPlatformStress(ctx context.Context, baseDir string, opts platform.PlatformStressOptions, logFunc func(string)) (string, error)
|
||||||
RunNCCLTests(ctx context.Context, baseDir string, gpuIndices []int, logFunc func(string)) (string, error)
|
RunNCCLTests(ctx context.Context, baseDir string, gpuIndices []int, logFunc func(string)) (string, error)
|
||||||
|
RunScenario(ctx context.Context, baseDir string, spec platform.ScenarioSpec, logFunc func(string)) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type runtimeChecker interface {
|
type runtimeChecker interface {
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ func (a *App) ListRemovableTargets() ([]platform.RemovableTarget, error) {
|
|||||||
return a.exports.ListRemovableTargets()
|
return a.exports.ListRemovableTargets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListScenarioFilesOnRemovableMedia lists scenarios/*.json found on any
|
||||||
|
// mounted removable target (e.g. the blackbox USB stick) — see
|
||||||
|
// platform.System.ListScenarioFilesOnRemovableMedia.
|
||||||
|
func (a *App) ListScenarioFilesOnRemovableMedia() ([]platform.ScenarioFileOnRemovableMedia, error) {
|
||||||
|
return a.exports.ListScenarioFilesOnRemovableMedia()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadScenarioFromRemovableMedia reads scenarios/<name>.json from whichever
|
||||||
|
// mounted removable target has it — see
|
||||||
|
// platform.System.ReadScenarioFromRemovableMedia.
|
||||||
|
func (a *App) ReadScenarioFromRemovableMedia(name string) ([]byte, error) {
|
||||||
|
return a.exports.ReadScenarioFromRemovableMedia(name)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) ExportLatestAudit(target platform.RemovableTarget) (string, error) {
|
func (a *App) ExportLatestAudit(target platform.RemovableTarget) (string, error) {
|
||||||
if _, err := os.Stat(DefaultAuditJSONPath); err != nil {
|
if _, err := os.Stat(DefaultAuditJSONPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ func (a *App) RunNvidiaAcceptancePack(baseDir string, logFunc func(string)) (str
|
|||||||
return a.sat.RunNvidiaAcceptancePack(baseDir, logFunc)
|
return a.sat.RunNvidiaAcceptancePack(baseDir, logFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunScenario runs a user-authored ScenarioSpec (see
|
||||||
|
// platform.ParseScenarioJSON) — the webui Scenario page and `bee run` CLI
|
||||||
|
// command both go through this so a scenario run gets the same SAT base
|
||||||
|
// directory default as every other pack.
|
||||||
|
func (a *App) RunScenario(ctx context.Context, baseDir string, spec platform.ScenarioSpec, logFunc func(string)) (string, error) {
|
||||||
|
if strings.TrimSpace(baseDir) == "" {
|
||||||
|
baseDir = DefaultSATBaseDir
|
||||||
|
}
|
||||||
|
return a.sat.RunScenario(ctx, baseDir, spec, logFunc)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) RunNvidiaAcceptancePackResult(baseDir string) (ActionResult, error) {
|
func (a *App) RunNvidiaAcceptancePackResult(baseDir string) (ActionResult, error) {
|
||||||
path, err := a.RunNvidiaAcceptancePack(baseDir, nil)
|
path, err := a.RunNvidiaAcceptancePack(baseDir, nil)
|
||||||
body := "Archive written."
|
body := "Archive written."
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ func (f fakeExports) ExportFileToTarget(src string, target platform.RemovableTar
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f fakeExports) ListScenarioFilesOnRemovableMedia() ([]platform.ScenarioFileOnRemovableMedia, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeExports) ReadScenarioFromRemovableMedia(name string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
type fakeRuntime struct {
|
type fakeRuntime struct {
|
||||||
collectFn func(string) (schema.RuntimeHealth, error)
|
collectFn func(string) (schema.RuntimeHealth, error)
|
||||||
dumpFn func(string) error
|
dumpFn func(string) error
|
||||||
@@ -332,6 +340,10 @@ func (f fakeSAT) RunNCCLTests(_ context.Context, baseDir string, gpuIndices []in
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f fakeSAT) RunScenario(_ context.Context, baseDir string, _ platform.ScenarioSpec, _ func(string)) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunNCCLTestsPassesSelectedGPUs(t *testing.T) {
|
func TestRunNCCLTestsPassesSelectedGPUs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -203,3 +203,47 @@ func (s *System) ReadScenarioFromRemovableMedia(name string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("scenarios/%s.json not found on any removable media", name)
|
return nil, fmt.Errorf("scenarios/%s.json not found on any removable media", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScenarioFileOnRemovableMedia is one scenarios/*.json file found on a
|
||||||
|
// mounted removable target, as returned by ListScenarioFilesOnRemovableMedia.
|
||||||
|
type ScenarioFileOnRemovableMedia struct {
|
||||||
|
Name string // filename without ".json" — what ReadScenarioFromRemovableMedia/bee run expects
|
||||||
|
Device string // which removable target it was found on
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListScenarioFilesOnRemovableMedia mounts each removable target in turn
|
||||||
|
// (unmounting again afterward if it mounted it itself) and collects every
|
||||||
|
// *.json file under a top-level scenarios/ directory. Used by the webui's
|
||||||
|
// Scenario page to show what's available to run without requiring the
|
||||||
|
// operator to already know a file's exact name.
|
||||||
|
func (s *System) ListScenarioFilesOnRemovableMedia() ([]ScenarioFileOnRemovableMedia, error) {
|
||||||
|
targets, err := s.ListRemovableTargets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []ScenarioFileOnRemovableMedia
|
||||||
|
for _, target := range targets {
|
||||||
|
mountpoint, mountedHere, mountErr := mountRemovableTargetReadOnly(target)
|
||||||
|
if mountErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries, readErr := os.ReadDir(filepath.Join(mountpoint, "scenarios"))
|
||||||
|
if readErr == nil {
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, ScenarioFileOnRemovableMedia{
|
||||||
|
Name: strings.TrimSuffix(e.Name(), ".json"),
|
||||||
|
Device: target.Device,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mountedHere {
|
||||||
|
_, _ = exportExecCommand("umount", mountpoint).CombinedOutput()
|
||||||
|
_ = os.Remove(mountpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func defaultTaskPriority(target string, params taskParams) int {
|
|||||||
return taskPriorityAudit
|
return taskPriorityAudit
|
||||||
case "nvidia-bench-perf", "nvidia-bench-power", "nvidia-bench-autotune":
|
case "nvidia-bench-perf", "nvidia-bench-power", "nvidia-bench-autotune":
|
||||||
return taskPriorityBenchmark
|
return taskPriorityBenchmark
|
||||||
case "nvidia-stress", "amd-stress", "memory-stress", "sat-stress", "platform-stress", "nvidia-compute":
|
case "nvidia-stress", "amd-stress", "memory-stress", "sat-stress", "platform-stress", "nvidia-compute", "scenario":
|
||||||
return taskPriorityBurn
|
return taskPriorityBurn
|
||||||
case "nvidia", "nvidia-targeted-stress", "nvidia-targeted-power", "nvidia-pulse",
|
case "nvidia", "nvidia-targeted-stress", "nvidia-targeted-power", "nvidia-pulse",
|
||||||
"nvidia-interconnect", "nvidia-bandwidth", "memory", "storage", "cpu",
|
"nvidia-interconnect", "nvidia-bandwidth", "memory", "storage", "cpu",
|
||||||
@@ -575,6 +575,61 @@ func (h *handler) handleAPISATRun(target string) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Scenario ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (h *handler) handleAPIScenarioList(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
if h.opts.App == nil {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "app not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
files, err := h.opts.App.ListScenarioFilesOnRemovableMedia()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type scenarioFile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Device string `json:"device"`
|
||||||
|
}
|
||||||
|
out := make([]scenarioFile, 0, len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
out = append(out, scenarioFile{Name: f.Name, Device: f.Device})
|
||||||
|
}
|
||||||
|
writeJSON(w, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleAPIScenarioRun(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.opts.App == nil {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "app not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if r.Body != nil {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(body.Name)
|
||||||
|
if name == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "scenario name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := &Task{
|
||||||
|
ID: newJobID("scenario"),
|
||||||
|
Name: "Scenario: " + name,
|
||||||
|
Target: "scenario",
|
||||||
|
Priority: defaultTaskPriority("scenario", taskParams{}),
|
||||||
|
Status: TaskPending,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
t.params.ScenarioName = name
|
||||||
|
globalQueue.enqueue(t)
|
||||||
|
writeJSON(w, map[string]string{"task_id": t.ID, "job_id": t.ID})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *handler) handleAPIBenchmarkNvidiaRunKind(target string) http.HandlerFunc {
|
func (h *handler) handleAPIBenchmarkNvidiaRunKind(target string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.opts.App == nil {
|
if h.opts.App == nil {
|
||||||
|
|||||||
@@ -46,6 +46,55 @@ func TestHandleAPISATRunDecodesBodyWithoutContentLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleAPIScenarioRunRequiresName(t *testing.T) {
|
||||||
|
h := &handler{opts: HandlerOptions{App: &app.App{}}}
|
||||||
|
req := httptest.NewRequest("POST", "/api/scenario/run", strings.NewReader(`{}`))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAPIScenarioRun(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != 400 {
|
||||||
|
t.Fatalf("status=%d want 400, body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAPIScenarioRunEnqueuesTask(t *testing.T) {
|
||||||
|
globalQueue.mu.Lock()
|
||||||
|
originalTasks := globalQueue.tasks
|
||||||
|
globalQueue.tasks = nil
|
||||||
|
globalQueue.mu.Unlock()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
globalQueue.mu.Lock()
|
||||||
|
globalQueue.tasks = originalTasks
|
||||||
|
globalQueue.mu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
h := &handler{opts: HandlerOptions{App: &app.App{}}}
|
||||||
|
req := httptest.NewRequest("POST", "/api/scenario/run", strings.NewReader(`{"name":"power-watch"}`))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.handleAPIScenarioRun(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != 200 {
|
||||||
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
globalQueue.mu.Lock()
|
||||||
|
defer globalQueue.mu.Unlock()
|
||||||
|
if len(globalQueue.tasks) != 1 {
|
||||||
|
t.Fatalf("tasks=%d want 1", len(globalQueue.tasks))
|
||||||
|
}
|
||||||
|
task := globalQueue.tasks[0]
|
||||||
|
if task.Target != "scenario" {
|
||||||
|
t.Fatalf("target=%q want scenario", task.Target)
|
||||||
|
}
|
||||||
|
if task.params.ScenarioName != "power-watch" {
|
||||||
|
t.Fatalf("scenario name=%q want power-watch", task.params.ScenarioName)
|
||||||
|
}
|
||||||
|
if task.Priority != taskPriorityBurn {
|
||||||
|
t.Fatalf("priority=%d want %d", task.Priority, taskPriorityBurn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleAPIBlackboxStatusReturnsDisabledWhenStateMissing(t *testing.T) {
|
func TestHandleAPIBlackboxStatusReturnsDisabledWhenStateMissing(t *testing.T) {
|
||||||
h := &handler{opts: HandlerOptions{ExportDir: t.TempDir()}}
|
h := &handler{opts: HandlerOptions{ExportDir: t.TempDir()}}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ func layoutNav(active string, buildLabel string) string {
|
|||||||
{id: "load", label: "3. Load", href: "/load"},
|
{id: "load", label: "3. Load", href: "/load"},
|
||||||
{id: "burn", label: "4. Burn", href: "/burn"},
|
{id: "burn", label: "4. Burn", href: "/burn"},
|
||||||
{id: "benchmark", label: "5. Benchmark", href: "/benchmark"},
|
{id: "benchmark", label: "5. Benchmark", href: "/benchmark"},
|
||||||
|
{id: "scenario", label: "6. Scenario", href: "/scenario"},
|
||||||
{sep: true},
|
{sep: true},
|
||||||
{id: "tasks", label: "Tasks", href: "/tasks"},
|
{id: "tasks", label: "Tasks", href: "/tasks"},
|
||||||
{id: "tools", label: "Tools", href: "/tools"},
|
{id: "tools", label: "Tools", href: "/tools"},
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
// renderScenario renders the "6. Scenario" page: scenarios/*.json files
|
||||||
|
// found on any mounted removable media (e.g. the blackbox USB stick),
|
||||||
|
// each runnable with one click. A run is enqueued as a normal Task
|
||||||
|
// (target "scenario") — progress/logs live in Tasks like every other SAT
|
||||||
|
// pack, no separate live-output UI needed here.
|
||||||
|
//
|
||||||
|
// See audit/internal/platform/scenario.go for the scenario file format and
|
||||||
|
// scenarios/README.md for the drop-a-file-on-the-USB-stick workflow this
|
||||||
|
// page is built around.
|
||||||
|
func renderScenario(opts HandlerOptions) string {
|
||||||
|
return `<p style="color:var(--muted);font-size:13px;margin-bottom:16px">
|
||||||
|
Scripted, ad-hoc test scenarios — commands plus background samplers (IPMI
|
||||||
|
sensors, nvidia-smi, ...), described in a small JSON file instead of a
|
||||||
|
hardcoded test. Drop a <code><name>.json</code> file under
|
||||||
|
<code>scenarios/</code> on a removable drive (e.g. the same USB stick
|
||||||
|
already plugged in for blackbox) and it shows up below — no rebuild
|
||||||
|
needed. Runs are enqueued like any other task; watch progress in
|
||||||
|
<a href="/tasks">Tasks</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<span>Scenarios found on removable media</span>
|
||||||
|
<button class="btn btn-sm btn-secondary" type="button" onclick="scenarioRefresh()">↻ Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="scenario-list"><p style="color:var(--muted);font-size:13px">Loading...</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function scenarioRefresh() {
|
||||||
|
const list = document.getElementById('scenario-list');
|
||||||
|
list.innerHTML = '<p style="color:var(--muted);font-size:13px">Loading...</p>';
|
||||||
|
fetch('/api/scenario/list')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(files => {
|
||||||
|
if (!files || !files.length) {
|
||||||
|
list.innerHTML = '<p style="color:var(--muted);font-size:13px">No scenarios/*.json found on any mounted removable media.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = '<table class="table"><thead><tr><th>Name</th><th>Found on</th><th></th></tr></thead><tbody>';
|
||||||
|
for (const f of files) {
|
||||||
|
html += '<tr><td>' + escapeHTML(f.name) + '</td><td style="color:var(--muted);font-size:12px">' + escapeHTML(f.device) + '</td>'
|
||||||
|
+ '<td><button class="btn btn-sm btn-primary" onclick="scenarioRun(' + JSON.stringify(f.name) + ', this)">▶ Run</button></td></tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
list.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
list.innerHTML = '<p style="color:var(--crit-fg)">Failed to list scenarios: ' + escapeHTML(String(err)) + '</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(s) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = s;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scenarioRun(name, btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Starting...';
|
||||||
|
fetch('/api/scenario/run', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({name: name})
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) return r.text().then(t => { throw new Error(t || r.statusText); });
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(() => { window.location.href = '/tasks'; })
|
||||||
|
.catch(err => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Run';
|
||||||
|
alert('Failed to start scenario: ' + err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarioRefresh();
|
||||||
|
</script>`
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRenderPageScenarioRoutes(t *testing.T) {
|
||||||
|
body := renderPage("scenario", HandlerOptions{})
|
||||||
|
if !strings.Contains(body, "scenario-list") {
|
||||||
|
t.Fatalf("scenario page body missing scenario-list container:\n%s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayoutNavIncludesScenario(t *testing.T) {
|
||||||
|
nav := layoutNav("scenario", "dev")
|
||||||
|
if !strings.Contains(nav, `href="/scenario"`) || !strings.Contains(nav, "6. Scenario") {
|
||||||
|
t.Fatalf("nav missing Scenario item:\n%s", nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,10 @@ func renderPage(page string, opts HandlerOptions) string {
|
|||||||
pageID = "benchmark"
|
pageID = "benchmark"
|
||||||
title = "5. Benchmark"
|
title = "5. Benchmark"
|
||||||
body = renderBenchmark(opts)
|
body = renderBenchmark(opts)
|
||||||
|
case "scenario":
|
||||||
|
pageID = "scenario"
|
||||||
|
title = "6. Scenario"
|
||||||
|
body = renderScenario(opts)
|
||||||
case "tools":
|
case "tools":
|
||||||
pageID = "tools"
|
pageID = "tools"
|
||||||
title = "Tools"
|
title = "Tools"
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
|||||||
mux.HandleFunc("POST /api/bee-bench/nvidia/perf/run", h.handleAPIBenchmarkNvidiaRunKind("nvidia-bench-perf"))
|
mux.HandleFunc("POST /api/bee-bench/nvidia/perf/run", h.handleAPIBenchmarkNvidiaRunKind("nvidia-bench-perf"))
|
||||||
mux.HandleFunc("POST /api/bee-bench/nvidia/power/run", h.handleAPIBenchmarkNvidiaRunKind("nvidia-bench-power"))
|
mux.HandleFunc("POST /api/bee-bench/nvidia/power/run", h.handleAPIBenchmarkNvidiaRunKind("nvidia-bench-power"))
|
||||||
mux.HandleFunc("POST /api/bee-bench/nvidia/autotune/run", h.handleAPIBenchmarkAutotuneRun())
|
mux.HandleFunc("POST /api/bee-bench/nvidia/autotune/run", h.handleAPIBenchmarkAutotuneRun())
|
||||||
|
mux.HandleFunc("GET /api/scenario/list", h.handleAPIScenarioList)
|
||||||
|
mux.HandleFunc("POST /api/scenario/run", h.handleAPIScenarioRun)
|
||||||
mux.HandleFunc("GET /api/bee-bench/nvidia/autotune/status", h.handleAPIBenchmarkAutotuneStatus)
|
mux.HandleFunc("GET /api/bee-bench/nvidia/autotune/status", h.handleAPIBenchmarkAutotuneStatus)
|
||||||
mux.HandleFunc("GET /api/benchmark/results", h.handleAPIBenchmarkResults)
|
mux.HandleFunc("GET /api/benchmark/results", h.handleAPIBenchmarkResults)
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ type taskParams struct {
|
|||||||
RAIDDevices []string `json:"raid_devices,omitempty"`
|
RAIDDevices []string `json:"raid_devices,omitempty"`
|
||||||
RAIDArrayName string `json:"raid_array_name,omitempty"`
|
RAIDArrayName string `json:"raid_array_name,omitempty"`
|
||||||
RAIDSlot string `json:"raid_slot,omitempty"`
|
RAIDSlot string `json:"raid_slot,omitempty"`
|
||||||
|
ScenarioName string `json:"scenario_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type persistedTask struct {
|
type persistedTask struct {
|
||||||
@@ -988,6 +989,22 @@ func (q *taskQueue) runTask(t *Task, j *jobState, ctx context.Context) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
archive, err = a.RunNCCLTests(ctx, "", t.params.GPUIndices, j.append)
|
archive, err = a.RunNCCLTests(ctx, "", t.params.GPUIndices, j.append)
|
||||||
|
case "scenario":
|
||||||
|
if a == nil {
|
||||||
|
err = fmt.Errorf("app not configured")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
data, err = a.ReadScenarioFromRemovableMedia(t.params.ScenarioName)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var spec platform.ScenarioSpec
|
||||||
|
spec, err = platform.ParseScenarioJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
archive, err = a.RunScenario(ctx, "", spec, j.append)
|
||||||
case "nvidia-stress":
|
case "nvidia-stress":
|
||||||
if a == nil {
|
if a == nil {
|
||||||
err = fmt.Errorf("app not configured")
|
err = fmt.Errorf("app not configured")
|
||||||
|
|||||||
Reference in New Issue
Block a user