Gerätezuordnungen im Client anzeigen

This commit is contained in:
Dieter Lang 2026-08-11 12:51:17 +02:00
parent 80e4196e16
commit 084ca66c7b
5 changed files with 860 additions and 25 deletions

View file

@ -70,15 +70,23 @@ type Application struct {
// NewApplication creates a new client Application.
//
// No configuration is loaded and no network connection is established.
// Start must be called before the application can be used.
// The client configuration is loaded immediately. No network connection
// is established. Start must be called before the application connects to
// the server or starts the Runtime.
func NewApplication(configFile string) (*Application, error) {
if configFile == "" {
return nil, fmt.Errorf("configuration file is empty")
}
cfg, err := config.LoadClient(configFile)
if err != nil {
return nil, fmt.Errorf("load client configuration: %w", err)
}
return &Application{
configFile: configFile,
config: cfg,
}, nil
}
@ -203,7 +211,6 @@ func (a *Application) Close() error {
a.runtime = nil
a.client = nil
a.config = nil
a.devices = nil
a.runtimeDone = nil
@ -224,13 +231,10 @@ func (a *Application) Close() error {
return firstErr
}
///////////////////////////////////////////////////////////////////////////////
// Configuration
///////////////////////////////////////////////////////////////////////////////
// Config returns a copy of the currently loaded client configuration.
//
// It returns nil when the application has not been started.
// The configuration is available after NewApplication, even when the
// server connection has not yet been established.
func (a *Application) Config() *config.ClientConfig {
if a == nil {
return nil

View file

@ -9,18 +9,68 @@
* Beschreibung:
* Tests für die Anwendungsschicht des rs2322tcp-Clients.
*
* Die Tests prüfen zunächst den Lebenszyklus der Application, ohne dafür
* eine echte Serververbindung aufbauen zu müssen.
* Die Tests prüfen den Lebenszyklus der Application und die lokale
* Konfigurationsverwaltung, ohne dafür eine echte Serververbindung
* aufbauen zu müssen.
*
* Insbesondere wird sichergestellt, dass eine neu erzeugte Application
* zunächst keinen laufenden Client und keine laufende Runtime besitzt und
* dass Close auch vor einem Start sicher verwendet werden kann.
* ihre lokale Konfiguration bereits kennt, obwohl noch keine Verbindung
* zum Server besteht.
* ============================================================================
*/
package client
import "testing"
import (
"os"
"path/filepath"
"testing"
"git.lang-dieter.de/rs2322tcp/internal/config"
)
///////////////////////////////////////////////////////////////////////////////
// Test helpers
///////////////////////////////////////////////////////////////////////////////
// writeTestConfig creates a temporary client configuration file.
func writeTestConfig(t *testing.T) string {
t.Helper()
dir := t.TempDir()
filename := filepath.Join(dir, "client.json")
content := `{
"server": {
"address": "127.0.0.1",
"port": 5000
},
"virtual_port_range": {
"first": 100,
"last": 199
},
"virtual_ports": [
{
"port": "/dev/ttyUSB100",
"remote_device": "radio"
},
{
"port": "/dev/ttyUSB101",
"remote_device": "rotor"
}
]
}`
if err := os.WriteFile(
filename,
[]byte(content),
0600,
); err != nil {
t.Fatalf("write test configuration: %v", err)
}
return filename
}
///////////////////////////////////////////////////////////////////////////////
// NewApplication
@ -42,11 +92,14 @@ func TestNewApplicationRejectsEmptyConfigFile(t *testing.T) {
}
// TestNewApplication verifies that a valid configuration-file path creates
// an application without starting it.
// an application and loads the local configuration without starting the
// network connection.
func TestNewApplication(t *testing.T) {
t.Helper()
application, err := NewApplication("client.json")
configFile := writeTestConfig(t)
application, err := NewApplication(configFile)
if err != nil {
t.Fatalf("NewApplication: %v", err)
}
@ -59,28 +112,115 @@ func TestNewApplication(t *testing.T) {
t.Fatal("new application reports running")
}
if application.Config() != nil {
t.Fatal("new application returned a configuration")
cfg := application.Config()
if cfg == nil {
t.Fatal("new application returned no configuration")
}
if len(cfg.VirtualPorts) != 2 {
t.Fatalf(
"new application returned %d virtual ports, want 2",
len(cfg.VirtualPorts),
)
}
if cfg.VirtualPorts[0].Port != "/dev/ttyUSB100" {
t.Errorf(
"virtual port 0 = %q, want %q",
cfg.VirtualPorts[0].Port,
"/dev/ttyUSB100",
)
}
if cfg.VirtualPorts[0].RemoteDevice != "radio" {
t.Errorf(
"remote device 0 = %q, want %q",
cfg.VirtualPorts[0].RemoteDevice,
"radio",
)
}
if cfg.VirtualPorts[1].Port != "/dev/ttyUSB101" {
t.Errorf(
"virtual port 1 = %q, want %q",
cfg.VirtualPorts[1].Port,
"/dev/ttyUSB101",
)
}
if cfg.VirtualPorts[1].RemoteDevice != "rotor" {
t.Errorf(
"remote device 1 = %q, want %q",
cfg.VirtualPorts[1].RemoteDevice,
"rotor",
)
}
if devices := application.Devices(); len(devices) != 0 {
t.Fatalf(
"new application returned %d devices",
"new application returned %d devices, want 0",
len(devices),
)
}
}
///////////////////////////////////////////////////////////////////////////////
// Configuration copy
///////////////////////////////////////////////////////////////////////////////
// TestApplicationConfigReturnsCopy verifies that modifying the returned
// configuration does not modify the Application's internal configuration.
func TestApplicationConfigReturnsCopy(t *testing.T) {
t.Helper()
configFile := writeTestConfig(t)
application, err := NewApplication(configFile)
if err != nil {
t.Fatalf("NewApplication: %v", err)
}
cfg := application.Config()
if cfg == nil {
t.Fatal("Config returned nil")
}
cfg.VirtualPorts[0].Port = "/dev/ttyUSB999"
cfg.VirtualPorts[0].RemoteDevice = "changed"
current := application.Config()
if current == nil {
t.Fatal("second Config returned nil")
}
if current.VirtualPorts[0].Port != "/dev/ttyUSB100" {
t.Errorf(
"internal port changed to %q",
current.VirtualPorts[0].Port,
)
}
if current.VirtualPorts[0].RemoteDevice != "radio" {
t.Errorf(
"internal remote device changed to %q",
current.VirtualPorts[0].RemoteDevice,
)
}
}
///////////////////////////////////////////////////////////////////////////////
// Close
///////////////////////////////////////////////////////////////////////////////
// TestApplicationCloseBeforeStart verifies that Close can safely be called
// before the application has been started.
// before the application has been started and that the local configuration
// remains available.
func TestApplicationCloseBeforeStart(t *testing.T) {
t.Helper()
application, err := NewApplication("client.json")
configFile := writeTestConfig(t)
application, err := NewApplication(configFile)
if err != nil {
t.Fatalf("NewApplication: %v", err)
}
@ -92,14 +232,21 @@ func TestApplicationCloseBeforeStart(t *testing.T) {
if application.Running() {
t.Fatal("application reports running after Close")
}
if cfg := application.Config(); cfg == nil {
t.Fatal("configuration was lost after Close")
}
}
// TestApplicationCloseIsIdempotent verifies that Close can be called more
// than once without producing an error.
// than once without producing an error and without losing the local
// configuration.
func TestApplicationCloseIsIdempotent(t *testing.T) {
t.Helper()
application, err := NewApplication("client.json")
configFile := writeTestConfig(t)
application, err := NewApplication(configFile)
if err != nil {
t.Fatalf("NewApplication: %v", err)
}
@ -115,6 +262,42 @@ func TestApplicationCloseIsIdempotent(t *testing.T) {
if application.Running() {
t.Fatal("application reports running after repeated Close")
}
if cfg := application.Config(); cfg == nil {
t.Fatal("configuration was lost after repeated Close")
}
}
///////////////////////////////////////////////////////////////////////////////
// Invalid configuration
///////////////////////////////////////////////////////////////////////////////
// TestNewApplicationRejectsInvalidConfiguration verifies that an invalid
// configuration file is rejected immediately.
func TestNewApplicationRejectsInvalidConfiguration(t *testing.T) {
t.Helper()
dir := t.TempDir()
filename := filepath.Join(dir, "invalid.json")
if err := os.WriteFile(
filename,
[]byte(`{"invalid": true}`),
0600,
); err != nil {
t.Fatalf("write invalid configuration: %v", err)
}
application, err := NewApplication(filename)
if err == nil {
t.Fatal("NewApplication with invalid configuration returned no error")
}
if application != nil {
t.Fatal(
"NewApplication with invalid configuration returned an application",
)
}
}
///////////////////////////////////////////////////////////////////////////////
@ -155,3 +338,7 @@ func TestNilApplication(t *testing.T) {
t.Fatal("nil Application returned devices")
}
}
// Keep config imported explicitly so the test documents that the temporary
// file represents the normal ClientConfig structure.
var _ config.ClientConfig

View file

@ -27,6 +27,10 @@
* Dabei wird die bestehende technische Client-Anwendung beendet und
* anschließend vollständig neu gestartet. Dadurch wird die aktuelle
* client.json erneut geladen.
*
* Die Gerätezuordnung wird zunächst ausschließlich angezeigt. Änderungen
* an der Zuordnung und deren Speicherung werden in einem späteren
* Entwicklungsschritt ergänzt.
* ============================================================================
*/
package gui
@ -52,9 +56,10 @@ type App struct {
clientApplication *client.Application
statusLabel *widget.Label
errorLabel *widget.Label
reconnectButton *widget.Button
statusLabel *widget.Label
errorLabel *widget.Label
reconnectButton *widget.Button
assignmentButton *widget.Button
}
// NewApp creates the graphical rs2322tcp client application.
@ -90,6 +95,11 @@ func NewApp(
app.reconnect,
)
app.assignmentButton = widget.NewButton(
"Gerätezuordnung",
app.showAssignments,
)
app.buildContent()
return app, nil
@ -106,6 +116,7 @@ func (a *App) buildContent() {
title,
a.statusLabel,
a.errorLabel,
a.assignmentButton,
a.reconnectButton,
)
@ -209,6 +220,98 @@ func (a *App) reconnect() {
}()
}
// showAssignments displays the current local-to-remote device assignments.
//
// This first implementation is deliberately read-only. It obtains the
// configuration and the current server device list from client.Application
// and converts them through the GUI-independent assignment functions.
//
// No configuration is changed or written by this function.
func (a *App) showAssignments() {
if a == nil {
return
}
cfg := a.clientApplication.Config()
devices := a.clientApplication.Devices()
assignments := BuildAssignments(cfg, devices)
unassigned := UnassignedDevices(cfg, devices)
content := container.NewVBox(
widget.NewLabel("Lokale Zuordnungen"),
)
if len(assignments) == 0 {
content.Add(
widget.NewLabel("Keine lokalen Zuordnungen vorhanden."),
)
} else {
for _, assignment := range assignments {
status := "nicht verfügbar"
if assignment.Available {
status = "verfügbar"
}
content.Add(
widget.NewLabel(
fmt.Sprintf(
"%s -> %s (%s)",
assignment.LocalPort,
assignment.RemoteDevice,
status,
),
),
)
}
}
content.Add(widget.NewSeparator())
content.Add(
widget.NewLabel("Server-Geräte ohne lokale Zuordnung"),
)
if len(unassigned) == 0 {
content.Add(
widget.NewLabel(
"Keine nicht verwendeten Server-Geräte.",
),
)
} else {
for _, device := range unassigned {
content.Add(
widget.NewLabel(
fmt.Sprintf(
"%s noch nicht verwendet",
device.ID,
),
),
)
}
}
assignmentWindow := a.fyneApp.NewWindow(
"Gerätezuordnung",
)
assignmentWindow.Resize(fyne.NewSize(650, 450))
assignmentWindow.SetContent(
container.NewBorder(
nil,
widget.NewButton(
"Schließen",
func() {
assignmentWindow.Close()
},
),
nil,
nil,
container.NewVScroll(content),
),
)
assignmentWindow.Show()
}
// Close closes the technical client application.
//
// The Runtime and the server connection are closed before the GUI window

167
internal/gui/assignment.go Normal file
View file

@ -0,0 +1,167 @@
/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/gui/assignment.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* GUI-unabhängige Aufbereitung der Zuordnung zwischen lokalen virtuellen
* seriellen Schnittstellen und den vom rs2322tcp-Server angebotenen Geräten.
*
* Die Datei enthält bewusst keine Fyne-Abhängigkeiten.
*
* Die lokale Client-Konfiguration beschreibt ausschließlich die tatsächlich
* gewünschten Verbindungen. Physisch vorhandene /dev/ttyUSB-Schnittstellen
* außerhalb des von rs2322tcp verwalteten virtuellen Bereichs werden hier
* nicht betrachtet.
*
* Ein Server-Gerät kann daher in drei Zuständen erscheinen:
*
* 1. lokal zugeordnet und aktuell vom Server angeboten
* -> verfügbar / verbunden
*
* 2. lokal zugeordnet, aber momentan nicht vom Server angeboten
* -> nicht verfügbar
*
* 3. vom Server angeboten, aber keiner lokalen Schnittstelle zugeordnet
* -> noch nicht verwendet
*
* Nicht verwendete Server-Geräte werden nicht Bestandteil der
* Client-Konfiguration.
* ============================================================================
*/
package gui
import (
"git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport"
)
///////////////////////////////////////////////////////////////////////////////
// Device assignment
///////////////////////////////////////////////////////////////////////////////
// DeviceAssignment beschreibt eine bereits in der Client-Konfiguration
// vorhandene Zuordnung zwischen einer lokalen virtuellen Schnittstelle
// und einem entfernten Gerät.
//
// Available gibt an, ob das konfigurierte Remote-Gerät momentan in der
// vom Server gelieferten Geräteliste vorhanden ist.
//
// Available == false bedeutet nicht, dass die Konfiguration ungültig ist.
// Das Gerät kann lediglich momentan nicht vom Server angeboten werden.
type DeviceAssignment struct {
LocalPort string
RemoteDevice string
Available bool
}
///////////////////////////////////////////////////////////////////////////////
// Build assignments
///////////////////////////////////////////////////////////////////////////////
// BuildAssignments erzeugt die Zuordnungsansicht aus der bestehenden
// Client-Konfiguration und der aktuell vom Server gelieferten Geräteliste.
//
// Es werden ausschließlich die in der Client-Konfiguration vorhandenen
// lokalen virtuellen Ports berücksichtigt.
//
// Physisch vorhandene serielle Schnittstellen des lokalen Rechners werden
// hier nicht gesucht und nicht bewertet.
func BuildAssignments(
cfg *config.ClientConfig,
devices []transport.RemoteDeviceInfo,
) []DeviceAssignment {
if cfg == nil {
return nil
}
assignments := make([]DeviceAssignment, 0, len(cfg.VirtualPorts))
for _, virtualPort := range cfg.VirtualPorts {
assignments = append(assignments, DeviceAssignment{
LocalPort: virtualPort.Port,
RemoteDevice: virtualPort.RemoteDevice,
Available: remoteDeviceExists(
virtualPort.RemoteDevice,
devices,
),
})
}
return assignments
}
///////////////////////////////////////////////////////////////////////////////
// Unassigned devices
///////////////////////////////////////////////////////////////////////////////
// UnassignedDevices liefert die vom Server angebotenen Geräte, die aktuell
// keiner lokalen virtuellen Schnittstelle zugeordnet sind.
//
// Diese Geräte werden bewusst nicht in die Client-Konfiguration übernommen.
// Erst wenn der Anwender im GUI eine Zuordnung vornimmt, entsteht daraus
// ein Eintrag in ClientConfig.VirtualPorts.
func UnassignedDevices(
cfg *config.ClientConfig,
devices []transport.RemoteDeviceInfo,
) []transport.RemoteDeviceInfo {
assigned := assignedRemoteDevices(cfg)
unassigned := make([]transport.RemoteDeviceInfo, 0)
for _, device := range devices {
if assigned[device.ID] {
continue
}
unassigned = append(unassigned, device)
}
return unassigned
}
///////////////////////////////////////////////////////////////////////////////
// Internal helpers
///////////////////////////////////////////////////////////////////////////////
// assignedRemoteDevices erzeugt eine Menge der Remote-Geräte, die bereits
// einer lokalen virtuellen Schnittstelle zugeordnet sind.
//
// Die Remote-Device-ID ist der stabile Schlüssel der Zuordnung.
func assignedRemoteDevices(
cfg *config.ClientConfig,
) map[string]bool {
assigned := make(map[string]bool)
if cfg == nil {
return assigned
}
for _, virtualPort := range cfg.VirtualPorts {
if virtualPort.RemoteDevice == "" {
continue
}
assigned[virtualPort.RemoteDevice] = true
}
return assigned
}
// remoteDeviceExists prüft, ob ein Remote-Gerät mit der angegebenen ID in
// der aktuellen Geräteliste des Servers vorhanden ist.
func remoteDeviceExists(
id string,
devices []transport.RemoteDeviceInfo,
) bool {
for _, device := range devices {
if device.ID == id {
return true
}
}
return false
}

View file

@ -0,0 +1,374 @@
/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/gui/assignment_test.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Tests für die GUI-unabhängige Aufbereitung der Zuordnung zwischen lokalen
* virtuellen seriellen Schnittstellen und den vom rs2322tcp-Server
* angebotenen Geräten.
* ============================================================================
*/
package gui
import (
"testing"
"git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport"
)
///////////////////////////////////////////////////////////////////////////////
// Test helpers
///////////////////////////////////////////////////////////////////////////////
// testClientConfig creates a client configuration containing the supplied
// local-to-remote assignments.
func testClientConfig(
assignments ...config.VirtualPortConfig,
) *config.ClientConfig {
return &config.ClientConfig{
VirtualPorts: assignments,
}
}
// testRemoteDevices creates a list of remote devices from their IDs.
func testRemoteDevices(ids ...string) []transport.RemoteDeviceInfo {
devices := make([]transport.RemoteDeviceInfo, 0, len(ids))
for _, id := range ids {
devices = append(devices, transport.RemoteDeviceInfo{
ID: id,
Name: id,
})
}
return devices
}
///////////////////////////////////////////////////////////////////////////////
// BuildAssignments tests
///////////////////////////////////////////////////////////////////////////////
func TestBuildAssignments(t *testing.T) {
cfg := testClientConfig(
config.VirtualPortConfig{
Port: "/dev/ttyUSB100",
RemoteDevice: "radio",
},
config.VirtualPortConfig{
Port: "/dev/ttyUSB101",
RemoteDevice: "rotor",
},
)
devices := testRemoteDevices(
"radio",
"rotor",
"gps",
)
assignments := BuildAssignments(cfg, devices)
if len(assignments) != 2 {
t.Fatalf(
"BuildAssignments() returned %d assignments, want 2",
len(assignments),
)
}
if assignments[0].LocalPort != "/dev/ttyUSB100" {
t.Errorf(
"assignments[0].LocalPort = %q, want %q",
assignments[0].LocalPort,
"/dev/ttyUSB100",
)
}
if assignments[0].RemoteDevice != "radio" {
t.Errorf(
"assignments[0].RemoteDevice = %q, want %q",
assignments[0].RemoteDevice,
"radio",
)
}
if !assignments[0].Available {
t.Error("assignments[0].Available = false, want true")
}
if assignments[1].LocalPort != "/dev/ttyUSB101" {
t.Errorf(
"assignments[1].LocalPort = %q, want %q",
assignments[1].LocalPort,
"/dev/ttyUSB101",
)
}
if assignments[1].RemoteDevice != "rotor" {
t.Errorf(
"assignments[1].RemoteDevice = %q, want %q",
assignments[1].RemoteDevice,
"rotor",
)
}
if !assignments[1].Available {
t.Error("assignments[1].Available = false, want true")
}
}
func TestBuildAssignmentsUnavailableDevice(t *testing.T) {
cfg := testClientConfig(
config.VirtualPortConfig{
Port: "/dev/ttyUSB100",
RemoteDevice: "radio",
},
config.VirtualPortConfig{
Port: "/dev/ttyUSB101",
RemoteDevice: "rotor",
},
)
devices := testRemoteDevices("rotor")
assignments := BuildAssignments(cfg, devices)
if len(assignments) != 2 {
t.Fatalf(
"BuildAssignments() returned %d assignments, want 2",
len(assignments),
)
}
if assignments[0].RemoteDevice != "radio" {
t.Errorf(
"assignments[0].RemoteDevice = %q, want %q",
assignments[0].RemoteDevice,
"radio",
)
}
if assignments[0].Available {
t.Error("radio assignment marked available, want unavailable")
}
if assignments[1].RemoteDevice != "rotor" {
t.Errorf(
"assignments[1].RemoteDevice = %q, want %q",
assignments[1].RemoteDevice,
"rotor",
)
}
if !assignments[1].Available {
t.Error("rotor assignment marked unavailable, want available")
}
}
func TestBuildAssignmentsNilConfig(t *testing.T) {
devices := testRemoteDevices("radio")
assignments := BuildAssignments(nil, devices)
if assignments != nil {
t.Fatalf(
"BuildAssignments(nil, ...) = %#v, want nil",
assignments,
)
}
}
func TestBuildAssignmentsEmptyConfig(t *testing.T) {
cfg := testClientConfig()
devices := testRemoteDevices(
"radio",
"rotor",
)
assignments := BuildAssignments(cfg, devices)
if len(assignments) != 0 {
t.Fatalf(
"BuildAssignments() returned %d assignments, want 0",
len(assignments),
)
}
}
///////////////////////////////////////////////////////////////////////////////
// UnassignedDevices tests
///////////////////////////////////////////////////////////////////////////////
func TestUnassignedDevices(t *testing.T) {
cfg := testClientConfig(
config.VirtualPortConfig{
Port: "/dev/ttyUSB100",
RemoteDevice: "radio",
},
config.VirtualPortConfig{
Port: "/dev/ttyUSB101",
RemoteDevice: "rotor",
},
)
devices := testRemoteDevices(
"radio",
"rotor",
"gps",
"tnc",
)
unassigned := UnassignedDevices(cfg, devices)
if len(unassigned) != 2 {
t.Fatalf(
"UnassignedDevices() returned %d devices, want 2",
len(unassigned),
)
}
if unassigned[0].ID != "gps" {
t.Errorf(
"unassigned[0].ID = %q, want %q",
unassigned[0].ID,
"gps",
)
}
if unassigned[1].ID != "tnc" {
t.Errorf(
"unassigned[1].ID = %q, want %q",
unassigned[1].ID,
"tnc",
)
}
}
func TestUnassignedDevicesAllAssigned(t *testing.T) {
cfg := testClientConfig(
config.VirtualPortConfig{
Port: "/dev/ttyUSB100",
RemoteDevice: "radio",
},
config.VirtualPortConfig{
Port: "/dev/ttyUSB101",
RemoteDevice: "rotor",
},
)
devices := testRemoteDevices(
"radio",
"rotor",
)
unassigned := UnassignedDevices(cfg, devices)
if len(unassigned) != 0 {
t.Fatalf(
"UnassignedDevices() returned %d devices, want 0",
len(unassigned),
)
}
}
func TestUnassignedDevicesEmptyConfig(t *testing.T) {
cfg := testClientConfig()
devices := testRemoteDevices(
"radio",
"rotor",
)
unassigned := UnassignedDevices(cfg, devices)
if len(unassigned) != 2 {
t.Fatalf(
"UnassignedDevices() returned %d devices, want 2",
len(unassigned),
)
}
}
func TestUnassignedDevicesNilConfig(t *testing.T) {
devices := testRemoteDevices(
"radio",
"rotor",
)
unassigned := UnassignedDevices(nil, devices)
if len(unassigned) != 2 {
t.Fatalf(
"UnassignedDevices(nil, ...) returned %d devices, want 2",
len(unassigned),
)
}
}
func TestUnassignedDevicesEmptyServerList(t *testing.T) {
cfg := testClientConfig(
config.VirtualPortConfig{
Port: "/dev/ttyUSB100",
RemoteDevice: "radio",
},
)
unassigned := UnassignedDevices(cfg, nil)
if len(unassigned) != 0 {
t.Fatalf(
"UnassignedDevices() returned %d devices, want 0",
len(unassigned),
)
}
}
///////////////////////////////////////////////////////////////////////////////
// Existing configuration does not change
///////////////////////////////////////////////////////////////////////////////
func TestAssignmentFunctionsDoNotModifyConfig(t *testing.T) {
cfg := testClientConfig(
config.VirtualPortConfig{
Port: "/dev/ttyUSB100",
RemoteDevice: "radio",
},
)
devices := testRemoteDevices(
"radio",
"rotor",
"gps",
)
_ = BuildAssignments(cfg, devices)
_ = UnassignedDevices(cfg, devices)
if len(cfg.VirtualPorts) != 1 {
t.Fatalf(
"configuration contains %d virtual ports, want 1",
len(cfg.VirtualPorts),
)
}
if cfg.VirtualPorts[0].Port != "/dev/ttyUSB100" {
t.Errorf(
"configuration port = %q, want %q",
cfg.VirtualPorts[0].Port,
"/dev/ttyUSB100",
)
}
if cfg.VirtualPorts[0].RemoteDevice != "radio" {
t.Errorf(
"configuration remote device = %q, want %q",
cfg.VirtualPorts[0].RemoteDevice,
"radio",
)
}
}