Simplified speech settings.

This commit is contained in:
Storm Dragon committed 2026-10-24 05:27:16 -04:00
1 parent 03e2b31e09
commit 54722e985e
20 files changed
+403 -92

No files matched your search

+2 -1
View File
@@ -78,11 +78,12 @@ type preferencesWorker struct {
announceEchoPreview func(string, string)
catalog speech.Catalog
discover func(context.Context, string) (speech.Catalog, error)
readSpeechDefaults func(context.Context, string) (speech.Settings, error)
}
func newPreferencesWorker(logger *slog.Logger, settings config.Settings, updater any) *preferencesWorker {
return &preferencesWorker{logger: logger, settings: settings, globalSettings: settings, appSettings: make(map[string]config.Settings), updater: updater,
extensionRequests: make(chan readerSettingsCall, 16), extensions: make(map[string]*readerExtension), speechCommands: make(chan input.Command, 16), remoteChanges: make(chan remoteSettingsChange, 16), discover: speech.Discover, open: make(chan string, 1), applicationChanges: make(chan string, 1), done: make(chan struct{})}
extensionRequests: make(chan readerSettingsCall, 16), extensions: make(map[string]*readerExtension), speechCommands: make(chan input.Command, 16), remoteChanges: make(chan remoteSettingsChange, 16), discover: speech.Discover, readSpeechDefaults: speech.ReadDefaults, open: make(chan string, 1), applicationChanges: make(chan string, 1), done: make(chan struct{})}
}
func (worker *preferencesWorker) Start(ctx context.Context) error {
+8
View File
@@ -132,6 +132,14 @@ func (worker *preferencesWorker) initial_response(childCtx context.Context) (pre
if err != nil {
worker.logger.Debug("discover echo voices", "error", err)
}
if worker.editing.SpeechServerFactory != "hardwarefactory" {
defaultsCtx, cancelDefaults := context.WithTimeout(childCtx, 500*time.Millisecond)
initial.SpeechDefaults, err = worker.readSpeechDefaults(defaultsCtx, worker.socket)
cancelDefaults()
if err != nil {
worker.logger.Debug("read speech defaults", "error", err)
}
}
if worker.editApp == "" {
if err := worker.load_extension_pages(childCtx); err != nil {
worker.logger.Warn("load tentacle preference pages", "error", err)
+2
View File
@@ -32,6 +32,7 @@ func TestPreferencesInitialRetainsLargeVoiceCatalog(t *testing.T) {
}
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), config.DefaultSettings(), nil)
worker.discover = func(context.Context, string) (speech.Catalog, error) { return catalog, nil }
worker.readSpeechDefaults = func(context.Context, string) (speech.Settings, error) { return speech.Settings{}, nil }
response, err := worker.initial_response(t.Context())
if err != nil || !reflect.DeepEqual(response.Catalog, catalog) {
t.Fatalf("initial response lost discovered voices: err=%v", err)
@@ -41,6 +42,7 @@ func TestPreferencesInitialRetainsLargeVoiceCatalog(t *testing.T) {
func TestPreferencesWindowReportsHelperExit(t *testing.T) {
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), config.DefaultSettings(), nil)
worker.discover = func(context.Context, string) (speech.Catalog, error) { return nil, nil }
worker.readSpeechDefaults = func(context.Context, string) (speech.Settings, error) { return speech.Settings{}, nil }
// The test executable does not implement --preferences-ui. It exits before
// opening a dialog, as a broken helper would on the affected machine.
err := worker.window(t.Context())
+5 -8
View File
@@ -73,13 +73,14 @@ static gboolean key(GtkWidget *window, GdkEventKey *event, gpointer data) {
if (event->keyval == GDK_KEY_Escape) { action = 3; return TRUE; }
return FALSE;
}
static void defaults(GtkToggleButton *button, gpointer spin) {
gtk_widget_set_sensitive(GTK_WIDGET(spin), !gtk_toggle_button_get_active(button));
}
static void voice_value_changed(GtkWidget *control, gpointer data) {
(void)control; (void)data;
voice_changed = 1;
}
static void speech_number_changed(GtkWidget *control, gpointer data) {
g_object_set_data(G_OBJECT(control), "number-edited", GINT_TO_POINTER(1));
voice_value_changed(control, data);
}
static gboolean echo_control_focused(GtkWidget *control) {
GtkWidget *focus = gtk_window_get_focus(GTK_WINDOW(prefs_widget("window")));
return focus && (focus == control || gtk_widget_is_ancestor(focus, control));
@@ -176,11 +177,7 @@ int prefs_init(const char *xml) {
g_signal_connect(prefs_widget("window"), "key-press-event", G_CALLBACK(key), NULL);
const char *ids[] = {"rate", "pitch", "volume"};
for (int i = 0; i < 3; i++) {
gchar *id = g_strconcat(ids[i], "_default", NULL);
g_signal_connect(prefs_widget(id), "toggled", G_CALLBACK(defaults), prefs_widget(ids[i]));
g_signal_connect(prefs_widget(id), "toggled", G_CALLBACK(voice_value_changed), NULL);
g_signal_connect(prefs_widget(ids[i]), "value-changed", G_CALLBACK(voice_value_changed), NULL);
g_free(id);
g_signal_connect(prefs_widget(ids[i]), "value-changed", G_CALLBACK(speech_number_changed), NULL);
}
const char *voiceScales[] = {"rate", "pitch", "volume", "echoRateScale", "echoPitchScale", "echoVolumeScale"};
for (unsigned i = 0; i < G_N_ELEMENTS(voiceScales); i++) {
+2 -3
View File
@@ -12,10 +12,9 @@ void prefs_busy(int busy, int can_save);
void prefs_present(void);
void prefs_close(void);
void prefs_status(const char *message, int focus);
void prefs_number_set(const char *id, double value, int use_default);
void prefs_voice_defaults(int kind);
void prefs_number_set(const char *id, double value);
double prefs_number_value(const char *id);
int prefs_default(const char *id);
int prefs_number_edited(const char *id);
void prefs_choice_set(const char *id, int value);
int prefs_choice_get(const char *id);
void prefs_hardware_devices_clear(void);
+7 -22
View File
@@ -3,30 +3,15 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "native_internal.h"
void prefs_voice_defaults(int kind) {
const char *ids[] = {"rate_default", "pitch_default", "volume_default"};
const char *server[] = {"Use server default rate", "Use server default pitch", "Use server default volume"};
const char *inherited[] = {"Use Default voice rate", "Use Default voice pitch", "Use Default voice volume"};
for (unsigned i = 0; i < G_N_ELEMENTS(ids); i++) {
const char *label = kind == 0 ? server[i] : inherited[i];
if (kind == 1 && i == 1) label = "Use automatic uppercase pitch";
GtkButton *button = GTK_BUTTON(prefs_widget(ids[i]));
if (g_strcmp0(gtk_button_get_label(button), label) != 0) gtk_button_set_label(button, label);
}
}
void prefs_number_set(const char *id, double value, int use_default) {
gchar *check = g_strconcat(id, "_default", NULL);
gtk_range_set_value(GTK_RANGE(prefs_widget(id)), value);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(prefs_widget(check)), use_default);
gtk_widget_set_sensitive(prefs_widget(id), !use_default);
g_free(check);
void prefs_number_set(const char *id, double value) {
GtkWidget *control = prefs_widget(id);
gtk_range_set_value(GTK_RANGE(control), value);
// Loading an effective value must not turn inheritance into an override.
g_object_set_data(G_OBJECT(control), "number-edited", NULL);
}
double prefs_number_value(const char *id) { return gtk_range_get_value(GTK_RANGE(prefs_widget(id))); }
int prefs_default(const char *id) {
gchar *check = g_strconcat(id, "_default", NULL);
int result = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(prefs_widget(check)));
g_free(check); return result;
int prefs_number_edited(const char *id) {
return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(prefs_widget(id)), "number-edited"));
}
void prefs_choice_set(const char *id, int value) {
gchar *name = g_strdup_printf("%s_%d", id, value);
+12 -15
View File
@@ -114,24 +114,21 @@
<child><object class="GtkComboBoxText" id="speechFamilies"><property name="can-focus">False</property><items><item>Default</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">3</property></packing></child>
<child><object class="GtkComboBoxText" id="voiceTypesCombo"><property name="can-focus">False</property><items><item>Default</item><item>Uppercase</item><item>Hyperlink</item><item>System</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">4</property></packing></child>
<child><object class="GtkLabel"><property name="label">_Voice type:</property><property name="use-underline">True</property><property name="mnemonic-widget">voiceTypesCombo</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">4</property></packing></child>
<child><object class="GtkCheckButton" id="volume_default"><property name="label">Use server default volume</property><property name="can-focus">True</property></object><packing><property name="left-attach">0</property><property name="top-attach">5</property><property name="width">2</property></packing></child>
<child><object class="GtkLabel" id="volume_label"><property name="label">Vo_lume:</property><property name="use-underline">True</property><property name="mnemonic-widget">volume</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">6</property></packing></child>
<child><object class="GtkScale" id="volume"><property name="adjustment">volume_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">6</property></packing></child>
<child><object class="GtkCheckButton" id="pitch_default"><property name="label">Use server default pitch</property><property name="can-focus">True</property></object><packing><property name="left-attach">0</property><property name="top-attach">7</property><property name="width">2</property></packing></child>
<child><object class="GtkLabel" id="pitch_label"><property name="label">Pi_tch:</property><property name="use-underline">True</property><property name="mnemonic-widget">pitch</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">8</property></packing></child>
<child><object class="GtkScale" id="pitch"><property name="adjustment">pitch_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">8</property></packing></child>
<child><object class="GtkCheckButton" id="rate_default"><property name="label">Use server default rate</property><property name="can-focus">True</property></object><packing><property name="left-attach">0</property><property name="top-attach">9</property><property name="width">2</property></packing></child>
<child><object class="GtkLabel" id="rate_label"><property name="label">_Rate:</property><property name="use-underline">True</property><property name="mnemonic-widget">rate</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">10</property></packing></child>
<child><object class="GtkScale" id="rate"><property name="adjustment">rate_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">10</property></packing></child>
<child><object class="GtkLabel" id="volume_label"><property name="label">Vo_lume:</property><property name="use-underline">True</property><property name="mnemonic-widget">volume</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">5</property></packing></child>
<child><object class="GtkScale" id="volume"><property name="adjustment">volume_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">5</property></packing></child>
<child><object class="GtkLabel" id="pitch_label"><property name="label">Pi_tch:</property><property name="use-underline">True</property><property name="mnemonic-widget">pitch</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">6</property></packing></child>
<child><object class="GtkScale" id="pitch"><property name="adjustment">pitch_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">6</property></packing></child>
<child><object class="GtkLabel" id="rate_label"><property name="label">_Rate:</property><property name="use-underline">True</property><property name="mnemonic-widget">rate</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">7</property></packing></child>
<child><object class="GtkScale" id="rate"><property name="adjustment">rate_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">7</property></packing></child>
<child><object class="GtkFrame" id="capitalLettersFrame"><property name="label">Capital letters</property><child><object class="GtkBox"><property name="orientation">vertical</property><property name="spacing">6</property><property name="border-width">8</property>
<child><object class="GtkCheckButton" id="raisePitchForCapitals"><property name="label">Raise pitch for capital letters</property><property name="can-focus">True</property></object></child>
<child><object class="GtkCheckButton" id="beepForCapitals"><property name="label">Beep for capital letters</property><property name="can-focus">True</property></object></child>
<child><object class="GtkCheckButton" id="sayCapForCapitals"><property name="label">Say “cap” before capital letters</property><property name="can-focus">True</property></object></child>
</object></child></object><packing><property name="left-attach">0</property><property name="top-attach">11</property><property name="width">2</property></packing></child>
<child><object class="GtkEntry" id="hardwareDeviceEntry"><property name="can-focus">True</property><property name="tooltip-text">Serial device path; completion offers detected devices</property></object><packing><property name="left-attach">1</property><property name="top-attach">15</property></packing></child>
<child><object class="GtkLabel"><property name="label">Serial _device:</property><property name="use-underline">True</property><property name="mnemonic-widget">hardwareDeviceEntry</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">15</property></packing></child>
<child><object class="GtkLabel"><property name="label">Serial baud rate:</property><property name="mnemonic-widget">hardwareBaudCombo</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">17</property></packing></child>
<child><object class="GtkComboBoxText" id="hardwareBaudCombo"><property name="can-focus">False</property><items><item>1200</item><item>2400</item><item>4800</item><item>9600</item><item>19200</item><item>38400</item><item>57600</item><item>115200</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">17</property></packing></child>
</object></child></object><packing><property name="left-attach">0</property><property name="top-attach">8</property><property name="width">2</property></packing></child>
<child><object class="GtkEntry" id="hardwareDeviceEntry"><property name="can-focus">True</property><property name="tooltip-text">Serial device path; completion offers detected devices</property></object><packing><property name="left-attach">1</property><property name="top-attach">12</property></packing></child>
<child><object class="GtkLabel"><property name="label">Serial _device:</property><property name="use-underline">True</property><property name="mnemonic-widget">hardwareDeviceEntry</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">12</property></packing></child>
<child><object class="GtkLabel"><property name="label">Serial baud rate:</property><property name="mnemonic-widget">hardwareBaudCombo</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">14</property></packing></child>
<child><object class="GtkComboBoxText" id="hardwareBaudCombo"><property name="can-focus">False</property><items><item>1200</item><item>2400</item><item>4800</item><item>9600</item><item>19200</item><item>38400</item><item>57600</item><item>115200</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">14</property></packing></child>
<child>
<object class="GtkBox" id="speech_settings">
<property name="orientation">vertical</property>
@@ -169,7 +166,7 @@
<child><object class="GtkRadioButton" id="indentationSpeechAndBeepsButton"><property name="label">Speech and beeps</property><property name="can-focus">True</property><property name="group">indentationSpeechButton</property></object></child>
</object></child></object></child>
</object>
<packing><property name="left-attach">0</property><property name="top-attach">18</property><property name="width">2</property></packing>
<packing><property name="left-attach">0</property><property name="top-attach">15</property><property name="width">2</property></packing>
</child>
</object></child>
</object>
+1
View File
@@ -124,6 +124,7 @@ type Response struct {
Tentacles *tentacle.Snapshot `json:",omitempty"`
ChooseApplication bool
Catalog speech.Catalog
SpeechDefaults speech.Settings
CatalogPending bool // More catalog parts follow before the UI can initialize.
Application string
Present bool
+1
View File
@@ -17,6 +17,7 @@ type uiSession struct {
extensionPages []ExtensionPage
progressBar config.ProgressBarSettings
catalog speech.Catalog
speechDefaults speech.Settings
contractionTables []braille.TableOption
soundThemes []string
echoModules []string
+22 -9
View File
@@ -13,6 +13,7 @@ import "C"
import (
"errors"
"fmt"
"math"
"path/filepath"
"sort"
"strings"
@@ -20,6 +21,7 @@ import (
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/speech"
)
func (session *uiSession) setSpeechVoice(values Values) {
@@ -218,8 +220,8 @@ func (session *uiSession) refreshSpeechVoices() {
C.prefs_hardware_sensitivity(C.int(system))
if system == 0 {
session.reconcile_voice_families(session.selectedSpeechModule())
session.load_voice_editor()
}
session.load_voice_editor()
}
if system == 1 {
return
@@ -292,24 +294,32 @@ func set_speech_support(values Values) {
setCheck("indentationSpeechAndBeepsButton", mode == "both")
}
func set_speech_numbers(values Values, inherited config.Voice) {
func set_speech_numbers(values Values, inherited config.Voice, defaults speech.Settings) {
for _, item := range []struct {
id string
value *float64
inherited *float64
server *int
scale float64
offset float64
fallback float64
}{
{"rate", values.Rate, inherited.Rate, 50}, {"pitch", values.Pitch, inherited.AveragePitch, 5}, {"volume", values.Volume, inherited.Gain, 5},
{"rate", values.Rate, inherited.Rate, defaults.Rate, 2, 98, 50},
{"pitch", values.Pitch, inherited.AveragePitch, defaults.Pitch, 20, 90, 5},
{"volume", values.Volume, inherited.Gain, defaults.Volume, 15, 35, 5},
} {
id := C.CString(item.id)
value := item.fallback
if item.server != nil {
value = math.Round((float64(*item.server)+item.offset)/item.scale*10) / 10
}
if item.inherited != nil {
value = *item.inherited
}
if item.value != nil {
value = *item.value
}
C.prefs_number_set(id, C.double(value), boolean(item.value == nil))
C.prefs_number_set(id, C.double(value))
C.free(unsafe.Pointer(id))
}
}
@@ -388,15 +398,18 @@ func (session *uiSession) load_voice_editor() {
voice := session.voiceDrafts[session.activeVoiceType]
settings := config.Settings{RaisePitchForCapitals: true, DefaultVoice: session.voiceDrafts[0], UppercaseVoice: session.voiceDrafts[1], HyperlinkVoice: session.voiceDrafts[2], SystemVoice: session.voiceDrafts[3]}
kind := []string{"default", "uppercase", "hyperlink", "system"}[session.activeVoiceType]
set_speech_numbers(Values{Rate: voice.Rate, Pitch: voice.AveragePitch, Volume: voice.Gain}, config.EffectiveVoice(settings, kind))
C.prefs_voice_defaults(C.int(session.activeVoiceType))
defaults := session.speechDefaults
if session.activeSpeechSystem == 1 {
defaults = speech.Settings{}
}
set_speech_numbers(Values{Rate: voice.Rate, Pitch: voice.AveragePitch, Volume: voice.Gain}, config.EffectiveVoice(settings, kind), defaults)
if session.activeSpeechSystem != 1 {
session.populateSpeechLanguages(session.selectedSpeechModule(), "", voice.Family)
}
}
func (session *uiSession) read_voice_editor() (config.Voice, error) {
var voice config.Voice
voice := session.voiceDrafts[session.activeVoiceType]
for _, item := range []struct {
id string
target **float64
@@ -405,10 +418,10 @@ func (session *uiSession) read_voice_editor() (config.Voice, error) {
{"rate", &voice.Rate, 99}, {"pitch", &voice.AveragePitch, 9}, {"volume", &voice.Gain, 9},
} {
id := C.CString(item.id)
useDefault := C.prefs_default(id) != 0
edited := C.prefs_number_edited(id) != 0
value := float64(C.prefs_number_value(id))
C.free(unsafe.Pointer(id))
if useDefault {
if !edited {
continue
}
if !(value >= 0 && value <= item.max) {
+1 -1
View File
@@ -64,7 +64,7 @@ func Run(ctx context.Context, input io.Reader, output io.Writer) error {
C.prefs_set_application(name)
C.free(unsafe.Pointer(name))
}
session := &uiSession{catalog: initial.Catalog, soundThemes: []string{"default", "none"}}
session := &uiSession{catalog: initial.Catalog, speechDefaults: initial.SpeechDefaults, soundThemes: []string{"default", "none"}}
session.setValues(initial.Values)
if err := session.set_extension_pages(initial.ExtensionPages); err != nil {
return err
+57
View File
@@ -0,0 +1,57 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package speech
import (
"bufio"
"context"
"fmt"
"net"
"strconv"
)
// ReadDefaults queries a fresh client, leaving the speaking client's settings
// alone. Unsupported or invalid fields stay nil so preferences can use sensible
// display values. The caller bounds the entire optional query with a deadline.
func ReadDefaults(ctx context.Context, socket string) (Settings, error) {
var defaults Settings
endpoint, err := resolveServerAddress(socket)
if err != nil {
return defaults, err
}
conn, err := (&net.Dialer{Timeout: connectTimeout}).DialContext(ctx, endpoint.network, endpoint.address)
if err != nil {
return defaults, err
}
defer conn.Close()
finish, err := prepareIO(ctx, conn, ioTimeout)
if err != nil {
return defaults, err
}
defer finish()
client := &Client{conn: conn, reader: bufio.NewReader(conn)}
for _, field := range []struct {
name string
target **int
}{
{"RATE", &defaults.Rate}, {"PITCH", &defaults.Pitch}, {"VOLUME", &defaults.Volume},
} {
if _, err := fmt.Fprintf(conn, "GET %s\r\n", field.name); err != nil {
return defaults, err
}
response, err := client.readReply()
if err != nil {
return defaults, err
}
if response.code < 200 || response.code >= 300 || len(response.data) != 1 {
continue
}
value, err := strconv.Atoi(response.data[0])
if err == nil && value >= -100 && value <= 100 {
*field.target = &value
}
}
return defaults, nil
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package speech
import (
"context"
"testing"
"time"
)
func TestReadDefaults(t *testing.T) {
path, server := newSettingsServer(t, func(command string) string {
switch command {
case "GET RATE":
return "200-20\r\n200 OK\r\n"
case "GET PITCH":
return "200--10\r\n200 OK\r\n"
case "GET VOLUME":
return "200-100\r\n200 OK\r\n"
}
return ""
})
defaults, err := ReadDefaults(t.Context(), path)
if err != nil || defaults.Rate == nil || *defaults.Rate != 20 ||
defaults.Pitch == nil || *defaults.Pitch != -10 || defaults.Volume == nil || *defaults.Volume != 100 {
t.Fatalf("defaults=%+v, err=%v", defaults, err)
}
for _, command := range []string{"SET SELF RATE 20", "SET SELF PITCH -10", "SET SELF VOLUME 100", "SPEAK"} {
if server.saw(1, command) {
t.Fatalf("query changed speech: %s", command)
}
}
}
func TestReadDefaultsRejectsUnsupportedAndInvalidValues(t *testing.T) {
for _, response := range []string{"500 UNSUPPORTED\r\n", "200 OK\r\n", "200-invalid\r\n200 OK\r\n",
"200-101\r\n200 OK\r\n", "200-0\r\n200-1\r\n200 OK\r\n"} {
t.Run(response, func(t *testing.T) {
path, _ := newSettingsServer(t, func(string) string { return response })
defaults, err := ReadDefaults(t.Context(), path)
if err != nil || defaults.Rate != nil || defaults.Pitch != nil || defaults.Volume != nil {
t.Fatalf("invalid defaults=%+v, err=%v", defaults, err)
}
})
}
}
func TestReadDefaultsRetainsPartialValuesAndHonorsDeadline(t *testing.T) {
path, _ := newSettingsServer(t, func(command string) string {
if command == "GET RATE" {
return "200-0\r\n200 OK\r\n"
}
if command == "GET PITCH" {
return "200-unfinished\r\n"
}
return ""
})
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
defaults, err := ReadDefaults(ctx, path)
if err == nil || defaults.Rate == nil || *defaults.Rate != 0 || defaults.Pitch != nil || defaults.Volume != nil {
t.Fatalf("partial defaults=%+v, err=%v", defaults, err)
}
}