Gerätezuordnungen im Client anzeigen
This commit is contained in:
parent
80e4196e16
commit
084ca66c7b
5 changed files with 860 additions and 25 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
167
internal/gui/assignment.go
Normal 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
|
||||
}
|
||||
374
internal/gui/assignment_test.go
Normal file
374
internal/gui/assignment_test.go
Normal 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",
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue