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