Files

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")
}
}