142 lines
5.2 KiB
Go
142 lines
5.2 KiB
Go
// Fenrir console screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option)
|
|
// any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
// more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
|
|
package scripts
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func fixture_script(t *testing.T, body string) Script {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "script with spaces__-__KEY_A.sh")
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body+"\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Script{Path: path}
|
|
}
|
|
|
|
func receive_result(t *testing.T, runner *Runner) Result {
|
|
t.Helper()
|
|
select {
|
|
case result := <-runner.Results():
|
|
return result
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("script did not finish")
|
|
return Result{}
|
|
}
|
|
}
|
|
|
|
func TestRunnerPreservesArgumentsOutputAndNonzeroExit(t *testing.T) {
|
|
runner := New_runner(context.Background(), Limits{Workers: 1, Queue: 2, OutputBytes: 4096}, func(terminal int) string {
|
|
if terminal != 3 {
|
|
t.Error(terminal)
|
|
}
|
|
return "Username; echo not-a-shell"
|
|
})
|
|
defer runner.Close()
|
|
script := fixture_script(t, "echo \"$1\"\necho problem >&2\nexit 9")
|
|
if !runner.Submit(Invocation{Script: script, Terminal: 3}) {
|
|
t.Fatal("submit failed")
|
|
}
|
|
result := receive_result(t, runner)
|
|
if result.Err != nil || result.Stdout != "Username; echo not-a-shell\n" || result.Stderr != "problem\n" {
|
|
t.Fatal(result)
|
|
}
|
|
}
|
|
|
|
func TestRunnerDrainsFloodedOutputAndBoundsRetention(t *testing.T) {
|
|
runner := New_runner(context.Background(), Limits{Workers: 1, Queue: 1, OutputBytes: 64, Timeout: time.Second}, nil)
|
|
defer runner.Close()
|
|
script := fixture_script(t, "i=0; while [ \"$i\" -lt 10000 ]; do echo output; echo error >&2; i=$((i+1)); done")
|
|
runner.Submit(Invocation{Script: script})
|
|
result := receive_result(t, runner)
|
|
if result.Err != nil || !result.Truncated || len(result.Stdout) != 64 || len(result.Stderr) != 64 {
|
|
t.Fatal(result)
|
|
}
|
|
}
|
|
|
|
func TestRunnerTimeoutFreesWorkerAndCancellationInvalidatesQueuedWork(t *testing.T) {
|
|
runner := New_runner(context.Background(), Limits{Workers: 1, Queue: 2, OutputBytes: 4096, Timeout: 150 * time.Millisecond}, nil)
|
|
defer runner.Close()
|
|
slow := fixture_script(t, "sleep 30 &\nwait")
|
|
runner.Submit(Invocation{Script: slow})
|
|
if result := receive_result(t, runner); !errors.Is(result.Err, context.DeadlineExceeded) {
|
|
t.Fatal(result)
|
|
}
|
|
runner.Submit(Invocation{Script: fixture_script(t, "echo recovered")})
|
|
if result := receive_result(t, runner); result.Err != nil || result.Stdout != "recovered\n" {
|
|
t.Fatal(result)
|
|
}
|
|
generation := runner.Cancel()
|
|
marker := filepath.Join(t.TempDir(), "must-not-run")
|
|
runner.Submit(Invocation{Script: fixture_script(t, "touch '"+marker+"'")})
|
|
if result := receive_result(t, runner); !errors.Is(result.Err, context.Canceled) {
|
|
t.Fatal(result)
|
|
}
|
|
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
|
t.Fatal("stale invocation executed", err)
|
|
}
|
|
runner.Submit(Invocation{Script: fixture_script(t, "echo new-session"), Generation: generation})
|
|
if result := receive_result(t, runner); result.Stdout != "new-session\n" {
|
|
t.Fatal(result)
|
|
}
|
|
}
|
|
|
|
func TestRunnerBoundsInheritedPipeWaitAndShutdown(t *testing.T) {
|
|
runner := New_runner(context.Background(), Limits{Workers: 1, Queue: 1, OutputBytes: 4096}, nil)
|
|
script := fixture_script(t, "sleep 30 &\necho parent-finished")
|
|
runner.Submit(Invocation{Script: script})
|
|
result := receive_result(t, runner)
|
|
if result.Err == nil || !strings.Contains(result.Stdout, "parent-finished") {
|
|
t.Fatal(result)
|
|
}
|
|
runner.Submit(Invocation{Script: fixture_script(t, "sleep 30")})
|
|
started := time.Now()
|
|
runner.Close()
|
|
if time.Since(started) > time.Second {
|
|
t.Fatal("shutdown waited for script")
|
|
}
|
|
}
|
|
|
|
func TestRunnerFileChecksAndQueueBound(t *testing.T) {
|
|
runner := New_runner(context.Background(), Limits{Workers: 1, Queue: 1, OutputBytes: 32}, nil)
|
|
defer runner.Close()
|
|
path := filepath.Join(t.TempDir(), "not-executable")
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, fixture := range []struct{ path, want string }{{path, "Script file is not executable"}, {filepath.Dir(path), "Script source is not a valid file"}, {path + "missing", "Script file not found"}} {
|
|
runner.Submit(Invocation{Script: Script{Path: fixture.path}})
|
|
result := receive_result(t, runner)
|
|
if result.Err == nil || result.Err.Error() != fixture.want {
|
|
t.Fatal(result)
|
|
}
|
|
}
|
|
// Directly fill a runner without workers to prove admission cannot block.
|
|
bounded := &Runner{ctx: context.Background(), jobs: make(chan Invocation, 1)}
|
|
if !bounded.Submit(Invocation{}) || bounded.Submit(Invocation{}) {
|
|
t.Fatal("unbounded admission")
|
|
}
|
|
}
|