diff --git a/.golangci.yaml b/.golangci.yaml index f7af91c..aba326f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -3,31 +3,18 @@ linters: disable: - exportloopref # Deprecated - execinquery # Deprecated + - gomnd # Deprecated name of mnd - exhaustruct # Very noisy, hard to disable when want zero values - forcetypeassert # If its wrong, we want to know - nlreturn # Not desired - - gci - - gocritic - - godox - - gomnd + - godox # Yes. The project is in progress + - depguard # Not desired + - varnamelen # Not desired - gochecknoglobals - - gosec - - mnd - nonamedreturns - - paralleltest - - predeclared - testpackage - - thelper - - varnamelen - - wrapcheck - - wsl - - depguard - cyclop - - revive - funlen - - unparam - - exhaustive - - inamedparam linters-settings: ireturn: @@ -37,3 +24,15 @@ linters-settings: - stdlib - empty - generic + gci: + sections: + - standard + - default + - localmodule + mnd: + ignored-files: + - 'monitor/.*.go' # UI code has many magic layout numbers... + - 'main.go' # Main file allowed, as it is example code + paralleltest: + # eCAL uses global state, any test using Initialize can't be run in parallel + ignore-missing: true diff --git a/cmd/monitor/common.go b/cmd/monitor/common.go index a9e6808..65f2aaf 100644 --- a/cmd/monitor/common.go +++ b/cmd/monitor/common.go @@ -4,9 +4,10 @@ import ( "errors" "fmt" - "github.com/DownerCase/ecal-go/ecal/monitoring" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) var ( @@ -26,38 +27,40 @@ func NewTable(columns []table.Column) table.Model { type NavKeyMap map[string]func() tea.Cmd func (navKeys NavKeyMap) HandleMsg(msg tea.Msg) (cmd tea.Cmd, navigated bool) { - switch msg := msg.(type) { - case tea.KeyMsg: + if msg, ok := msg.(tea.KeyMsg); ok { if f, ok := navKeys[msg.String()]; ok { return f(), true } } + return nil, false } -type topicType int +type TopicType int const ( - topicTypeSubscriber topicType = iota + topicTypeSubscriber TopicType = iota topicTypePublisher ) -func getTopicMonitoring(topicType topicType) []monitoring.TopicMon { +func getTopicMonitoring(topicType TopicType) []monitoring.TopicMon { switch topicType { case topicTypeSubscriber: return monitoring.GetMonitoring(monitoring.MonitorSubscriber).Subscribers case topicTypePublisher: return monitoring.GetMonitoring(monitoring.MonitorPublisher).Publishers } + return nil } -func getTopicFromID(topicType topicType, id string) (monitoring.TopicMon, error) { +func getTopicFromID(topicType TopicType, id string) (monitoring.TopicMon, error) { topicList := getTopicMonitoring(topicType) for _, topic := range topicList { if topic.TopicID == id { return topic, nil } } + return monitoring.TopicMon{}, fmt.Errorf("[getTopicFromId]: %w", errNoTopic) } diff --git a/cmd/monitor/config_page.go b/cmd/monitor/config_page.go index a81e500..105dd96 100644 --- a/cmd/monitor/config_page.go +++ b/cmd/monitor/config_page.go @@ -1,31 +1,33 @@ package main import ( - "github.com/DownerCase/ecal-go/ecal" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal" ) -type modelConfig struct { +type ModelConfig struct { viewport viewport.Model } -func NewConfigModel() *modelConfig { +func NewConfigModel() *ModelConfig { viewport := viewport.New(85, 10) viewport.SetContent(ecal.GetConfig()) viewport.Style = baseStyle - return &modelConfig{ + + return &ModelConfig{ viewport: viewport, } } -func (m *modelConfig) Refresh() {} +func (m *ModelConfig) Refresh() {} -func (m *modelConfig) Update(msg tea.Msg) (cmd tea.Cmd) { +func (m *ModelConfig) Update(msg tea.Msg) (cmd tea.Cmd) { m.viewport, cmd = m.viewport.Update(msg) return cmd } -func (m *modelConfig) View() string { +func (m *ModelConfig) View() string { return m.viewport.View() } diff --git a/cmd/monitor/hosts_list.go b/cmd/monitor/hosts_list.go index 00a04f1..9cd2315 100644 --- a/cmd/monitor/hosts_list.go +++ b/cmd/monitor/hosts_list.go @@ -3,16 +3,17 @@ package main import ( "strconv" - "github.com/DownerCase/ecal-go/ecal/monitoring" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) -type modelHosts struct { +type ModelHosts struct { table table.Model } -func NewHostsModel() *modelHosts { +func NewHostsModel() *ModelHosts { columns := []table.Column{ {Title: "Host", Width: 28}, {Title: "Processes", Width: 9}, @@ -22,21 +23,21 @@ func NewHostsModel() *modelHosts { {Title: "Clients", Width: 7}, } - return &modelHosts{ + return &ModelHosts{ table: NewTable(columns), } } -func (m *modelHosts) Update(msg tea.Msg) tea.Cmd { - m.updateTable(nil) +func (m *ModelHosts) Update(msg tea.Msg) tea.Cmd { + m.updateTable(msg) return nil } -func (m *modelHosts) View() string { +func (m *ModelHosts) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.table.HelpView() } -func (m *modelHosts) Refresh() { +func (m *ModelHosts) Refresh() { m.updateTable(nil) } @@ -48,33 +49,37 @@ type hostInfo struct { Processes int } -func (m *modelHosts) updateTable(msg tea.Msg) { +func (m *ModelHosts) updateTable(msg tea.Msg) { mon := monitoring.GetMonitoring(monitoring.MonitorAll) hosts := make(map[string]hostInfo) for _, pub := range mon.Publishers { host := hosts[pub.HostName] - host.Publishers += 1 + host.Publishers++ hosts[pub.HostName] = host } + for _, sub := range mon.Subscribers { host := hosts[sub.HostName] - host.Subscribers += 1 + host.Subscribers++ hosts[sub.HostName] = host } + for _, client := range mon.Clients { host := hosts[client.HostName] - host.Clients += 1 + host.Clients++ hosts[client.HostName] = host } + for _, server := range mon.Servers { host := hosts[server.HostName] - host.Servers += 1 + host.Servers++ hosts[server.HostName] = host } + for _, proc := range mon.Processes { host := hosts[proc.HostName] - host.Processes += 1 + host.Processes++ hosts[proc.HostName] = host } @@ -93,5 +98,6 @@ func hostsToRows(hosts map[string]hostInfo) (rows []table.Row) { strconv.FormatInt(int64(hostInfo.Clients), 10), }) } + return } diff --git a/cmd/monitor/log_list.go b/cmd/monitor/log_list.go index 364277c..aabafb8 100644 --- a/cmd/monitor/log_list.go +++ b/cmd/monitor/log_list.go @@ -3,11 +3,12 @@ package main import ( "time" - "github.com/DownerCase/ecal-go/ecal/logging" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/logging" ) type LoggingPage int @@ -22,7 +23,7 @@ type logsKeyMap struct { Clear key.Binding } -type modelLogs struct { +type ModelLogs struct { table table.Model subpage LoggingPage help help.Model @@ -48,7 +49,7 @@ func (km logsKeyMap) FullHelp() [][]key.Binding { return append([][]key.Binding{{km.Clear}}, km.KeyMap.FullHelp()...) } -func NewLogsModel() *modelLogs { +func NewLogsModel() *ModelLogs { columns := []table.Column{ {Title: "Time", Width: 10}, {Title: "Level", Width: 6}, @@ -56,7 +57,7 @@ func NewLogsModel() *modelLogs { {Title: "Message", Width: 46}, } - return &modelLogs{ + return &ModelLogs{ table: NewTable(columns), subpage: subpageLoggingMain, help: help.New(), @@ -64,13 +65,12 @@ func NewLogsModel() *modelLogs { } } -func (m *modelLogs) Update(msg tea.Msg) tea.Cmd { +func (m *ModelLogs) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd switch m.subpage { case subpageLoggingMain: - switch msg := msg.(type) { - case tea.KeyMsg: + if msg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(msg, m.keymap.Clear): m.table.SetRows([]table.Row{}) @@ -80,37 +80,38 @@ func (m *modelLogs) Update(msg tea.Msg) tea.Cmd { } } case subpageLoggingDetailed: - // cmd = m.model_detailed.Update(msg) } + return cmd } -func (m *modelLogs) View() string { +func (m *ModelLogs) View() string { switch m.subpage { case subpageLoggingMain: return baseStyle.Render(m.table.View()) + "\n" + m.help.View(m.keymap) case subpageLoggingDetailed: - // return m.model_detailed.View() } + return "Invalid page" } -func (m *modelLogs) Refresh() { +func (m *ModelLogs) Refresh() { switch m.subpage { case subpageLoggingDetailed: // m.model_detailed.Refresh() - default: + case subpageLoggingMain: m.updateTable(nil) } } -func (m *modelLogs) updateTable(msg tea.Msg) { +func (m *ModelLogs) updateTable(msg tea.Msg) { rows := []table.Row{} logs := logging.GetLogging().Messages for _, log := range logs { rows = append(rows, logToRow(log)) } + m.table.SetRows(append(m.table.Rows(), rows...)) m.table, _ = m.table.Update(msg) } diff --git a/cmd/monitor/page_processes.go b/cmd/monitor/page_processes.go index e76913e..29db786 100644 --- a/cmd/monitor/page_processes.go +++ b/cmd/monitor/page_processes.go @@ -11,14 +11,14 @@ const ( subpageProcDetailed ) -type modelProcesses struct { +type ModelProcesses struct { subpage ProcessesPage pages map[ProcessesPage]PageModel NavKeys NavKeyMap } -func NewProcessesModel() *modelProcesses { - return (&modelProcesses{ +func NewProcessesModel() *ModelProcesses { + return (&ModelProcesses{ subpage: subpageProcMain, pages: map[ProcessesPage]PageModel{ subpageProcMain: NewProcessesMainModel(), @@ -28,45 +28,52 @@ func NewProcessesModel() *modelProcesses { }).Init() } -func (m *modelProcesses) Init() *modelProcesses { - m.NavKeys["esc"] = func() tea.Cmd { m.navUp(); return nil } - m.NavKeys["enter"] = func() tea.Cmd { m.navDown(); return nil } +func (m *ModelProcesses) Init() *ModelProcesses { + m.NavKeys["esc"] = func() tea.Cmd { return m.navUp() } + m.NavKeys["enter"] = func() tea.Cmd { return m.navDown() } + return m } -func (m *modelProcesses) Update(msg tea.Msg) tea.Cmd { +func (m *ModelProcesses) Update(msg tea.Msg) tea.Cmd { if cmd, navigated := m.NavKeys.HandleMsg(msg); navigated { return cmd } + return m.pages[m.subpage].Update(msg) } -func (m *modelProcesses) View() string { +func (m *ModelProcesses) View() string { return m.pages[m.subpage].View() } -func (m *modelProcesses) Refresh() { +func (m *ModelProcesses) Refresh() { m.pages[m.subpage].Refresh() } -func (m *modelProcesses) navDown() { - switch m.subpage { - case subpageProcMain: - main := m.pages[subpageProcMain].(*modelProcessesMain) +func (m *ModelProcesses) navDown() tea.Cmd { + if m.subpage == subpageProcMain { + main := m.pages[subpageProcMain].(*ModelProcessesMain) + pid, err := main.getSelectedPid() if err != nil { - return // Can't transition + return nil // Can't transition } - detailed := m.pages[subpageProcDetailed].(*modelProcessDetailed) + + detailed := m.pages[subpageProcDetailed].(*ModelProcessDetailed) detailed.Pid = pid m.subpage = subpageProcDetailed + detailed.Refresh() } + + return nil } -func (m *modelProcesses) navUp() { - switch m.subpage { - case subpageProcDetailed: +func (m *ModelProcesses) navUp() tea.Cmd { + if m.subpage == subpageProcDetailed { m.subpage = subpageProcMain } + + return nil } diff --git a/cmd/monitor/page_processes_detailed.go b/cmd/monitor/page_processes_detailed.go index 9351e54..950ed8b 100644 --- a/cmd/monitor/page_processes_detailed.go +++ b/cmd/monitor/page_processes_detailed.go @@ -4,60 +4,61 @@ import ( "fmt" "strconv" - "github.com/DownerCase/ecal-go/ecal/monitoring" - "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) -type modelProcessDetailed struct { +type ModelProcessDetailed struct { table table.Model Pid int32 } -func NewDetailedProcessModel() *modelProcessDetailed { +func NewDetailedProcessModel() *ModelProcessDetailed { cols := []table.Column{ {Title: "", Width: 10}, {Title: "", Width: 67}, } - return &modelProcessDetailed{ + return &ModelProcessDetailed{ table: NewTable(cols), Pid: 0, } } -func (m *modelProcessDetailed) Init() tea.Cmd { +func (m *ModelProcessDetailed) Init() tea.Cmd { return nil } -func (m *modelProcessDetailed) Update(msg tea.Msg) tea.Cmd { +func (m *ModelProcessDetailed) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: + if msg, ok := msg.(tea.KeyMsg); ok { m.table, cmd = m.table.Update(msg) } + return cmd } -func (m *modelProcessDetailed) View() string { +func (m *ModelProcessDetailed) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.table.HelpView() } -func (m *modelProcessDetailed) Refresh() { +func (m *ModelProcessDetailed) Refresh() { m.updateDetailedTable(nil) } -func (m *modelProcessDetailed) updateDetailedTable(msg tea.Msg) { - mon := monitoring.GetMonitoring(monitoring.MonitorProcess) +func (m *ModelProcessDetailed) updateDetailedTable(msg tea.Msg) { var p monitoring.ProcessMon + mon := monitoring.GetMonitoring(monitoring.MonitorProcess) for _, proc := range mon.Processes { if proc.Pid == m.Pid { p = proc break } } + m.table.Columns()[0].Title = strconv.FormatInt(int64(p.Pid), 10) m.table.Columns()[1].Title = p.ProcessName health := fmt.Sprintf("%s %v", p.StateSeverity.String(), p.StateSeverityLevel) diff --git a/cmd/monitor/page_processes_main.go b/cmd/monitor/page_processes_main.go index 10c60b4..f20be55 100644 --- a/cmd/monitor/page_processes_main.go +++ b/cmd/monitor/page_processes_main.go @@ -4,16 +4,17 @@ import ( "path/filepath" "strconv" - "github.com/DownerCase/ecal-go/ecal/monitoring" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) -type modelProcessesMain struct { +type ModelProcessesMain struct { table table.Model } -func NewProcessesMainModel() *modelProcessesMain { +func NewProcessesMainModel() *ModelProcessesMain { columns := []table.Column{ {Title: "PID", Width: 7}, {Title: "Name", Width: 33}, @@ -22,40 +23,45 @@ func NewProcessesMainModel() *modelProcessesMain { {Title: "Tick", Width: 4}, } - return &modelProcessesMain{ + return &ModelProcessesMain{ table: NewTable(columns), } } -func (m *modelProcessesMain) Update(msg tea.Msg) tea.Cmd { +func (m *ModelProcessesMain) Update(msg tea.Msg) tea.Cmd { return m.updateTable(msg) } -func (m *modelProcessesMain) View() string { +func (m *ModelProcessesMain) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.table.HelpView() } -func (m *modelProcessesMain) Refresh() { +func (m *ModelProcessesMain) Refresh() { m.updateTable(nil) } -func (m *modelProcessesMain) getSelectedPid() (int32, error) { +func (m *ModelProcessesMain) getSelectedPid() (int32, error) { row := m.table.SelectedRow() if row == nil { return 0, errEmptyTable } - pid, err := strconv.ParseInt(row[0], 10, 64) + + pid, err := strconv.ParseInt(row[0], 10, 32) + return int32(pid), err } -func (m *modelProcessesMain) updateTable(msg tea.Msg) (cmd tea.Cmd) { +func (m *ModelProcessesMain) updateTable(msg tea.Msg) (cmd tea.Cmd) { rows := []table.Row{} + mon := monitoring.GetMonitoring(monitoring.MonitorProcess) for _, proc := range mon.Processes { rows = append(rows, procToRow(proc)) } + m.table.SetRows(rows) m.table, cmd = m.table.Update(msg) + return } diff --git a/cmd/monitor/page_services.go b/cmd/monitor/page_services.go index ca808d6..e8e707d 100644 --- a/cmd/monitor/page_services.go +++ b/cmd/monitor/page_services.go @@ -11,14 +11,14 @@ const ( subpageServicesDetailed ) -type modelServices struct { +type ModelServices struct { subpage ServicesPage pages map[ServicesPage]PageModel NavKeys NavKeyMap } -func NewServicesModel() *modelServices { - return (&modelServices{ +func NewServicesModel() *ModelServices { + return (&ModelServices{ subpage: subpageServicesMain, pages: map[ServicesPage]PageModel{ subpageServicesMain: NewServicesMainModel(), @@ -28,46 +28,53 @@ func NewServicesModel() *modelServices { }).Init() } -func (m *modelServices) Refresh() { +func (m *ModelServices) Refresh() { m.pages[m.subpage].Refresh() } -func (m *modelServices) Init() *modelServices { - m.NavKeys["esc"] = func() tea.Cmd { m.navUp(); return nil } - m.NavKeys["enter"] = func() tea.Cmd { m.navDown(); return nil } +func (m *ModelServices) Init() *ModelServices { + m.NavKeys["esc"] = func() tea.Cmd { return m.navUp() } + m.NavKeys["enter"] = func() tea.Cmd { return m.navDown() } + return m } -func (m *modelServices) Update(msg tea.Msg) tea.Cmd { +func (m *ModelServices) Update(msg tea.Msg) tea.Cmd { if cmd, navigated := m.NavKeys.HandleMsg(msg); navigated { return cmd } + return m.pages[m.subpage].Update(msg) } -func (m *modelServices) View() string { +func (m *ModelServices) View() string { return m.pages[m.subpage].View() } -func (m *modelServices) navDown() { - switch m.subpage { - case subpageServicesMain: - main := m.pages[subpageServicesMain].(*modelServicesMain) +func (m *ModelServices) navDown() tea.Cmd { + if m.subpage == subpageServicesMain { + main := m.pages[subpageServicesMain].(*ModelServicesMain) + id, isServer, err := main.GetSelectedID() if err != nil { - return // Can't transition + return nil // Can't transition } - detailed := m.pages[subpageServicesDetailed].(*modelServiceDetailed) + + detailed := m.pages[subpageServicesDetailed].(*ModelServiceDetailed) detailed.IsServer = isServer detailed.ID = id detailed.Refresh() + m.subpage = subpageServicesDetailed } + + return nil } -func (m *modelServices) navUp() { - switch m.subpage { - case subpageServicesDetailed: +func (m *ModelServices) navUp() tea.Cmd { + if m.subpage == subpageServicesDetailed { m.subpage = subpageServicesMain } + + return nil } diff --git a/cmd/monitor/page_services_detailed.go b/cmd/monitor/page_services_detailed.go index 5e886bd..65fa6a7 100644 --- a/cmd/monitor/page_services_detailed.go +++ b/cmd/monitor/page_services_detailed.go @@ -4,48 +4,48 @@ import ( "fmt" "strconv" - "github.com/DownerCase/ecal-go/ecal/monitoring" - "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) -type modelServiceDetailed struct { +type ModelServiceDetailed struct { table table.Model ID string IsServer bool } -func NewDetailedServiceModel() *modelServiceDetailed { +func NewDetailedServiceModel() *ModelServiceDetailed { cols := []table.Column{ {Title: "", Width: 10}, {Title: "", Width: 67}, } - return &modelServiceDetailed{ + return &ModelServiceDetailed{ table: NewTable(cols), ID: "", } } -func (m *modelServiceDetailed) Update(msg tea.Msg) tea.Cmd { +func (m *ModelServiceDetailed) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: + if msg, ok := msg.(tea.KeyMsg); ok { m.table, cmd = m.table.Update(msg) } + return cmd } -func (m *modelServiceDetailed) View() string { +func (m *ModelServiceDetailed) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.table.HelpView() } -func (m *modelServiceDetailed) Refresh() { +func (m *ModelServiceDetailed) Refresh() { m.updateDetailedTable(nil) } -func (m *modelServiceDetailed) updateDetailedTable(msg tea.Msg) { +func (m *ModelServiceDetailed) updateDetailedTable(msg tea.Msg) { if m.IsServer { mon := monitoring.GetMonitoring(monitoring.MonitorServer) for _, s := range mon.Servers { @@ -63,17 +63,18 @@ func (m *modelServiceDetailed) updateDetailedTable(msg tea.Msg) { } } } + m.table, _ = m.table.Update(msg) } -func (m *modelServiceDetailed) updateTableClient(c *monitoring.ClientMon) { +func (m *ModelServiceDetailed) updateTableClient(c *monitoring.ClientMon) { m.table.Columns()[0].Title = "Client" m.table.Columns()[1].Title = c.Process baseRows := m.getBaseRows(c.ServiceBase) m.table.SetRows(append(baseRows, getMethodRows(c.ServiceBase)...)) } -func (m *modelServiceDetailed) updateTableServer(s *monitoring.ServerMon) { +func (m *ModelServiceDetailed) updateTableServer(s *monitoring.ServerMon) { m.table.Columns()[0].Title = "Server" m.table.Columns()[1].Title = s.Process baseRows := m.getBaseRows(s.ServiceBase) @@ -81,7 +82,7 @@ func (m *modelServiceDetailed) updateTableServer(s *monitoring.ServerMon) { m.table.SetRows(append(baseRows, getMethodRows(s.ServiceBase)...)) } -func (m *modelServiceDetailed) getBaseRows(b monitoring.ServiceBase) []table.Row { +func (m *ModelServiceDetailed) getBaseRows(b monitoring.ServiceBase) []table.Row { return []table.Row{ {"Unit", fmt.Sprintf("%s (Protocol V%v)", b.Unit, b.ProtocolVersion)}, {"Pid", fmt.Sprintf("%v (%s)", b.Pid, b.HostName)}, @@ -99,5 +100,6 @@ func getMethodRows(b monitoring.ServiceBase) []table.Row { fmt.Sprintf("%s -> %s (Called x%v)", method.RequestType.Type, method.ResponseType.Type, method.CallCount), }) } + return rows } diff --git a/cmd/monitor/page_services_main.go b/cmd/monitor/page_services_main.go index c3c24af..c0a82c4 100644 --- a/cmd/monitor/page_services_main.go +++ b/cmd/monitor/page_services_main.go @@ -3,16 +3,17 @@ package main import ( "strconv" - "github.com/DownerCase/ecal-go/ecal/monitoring" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) -type modelServicesMain struct { +type ModelServicesMain struct { table table.Model } -func NewServicesMainModel() *modelServicesMain { +func NewServicesMainModel() *ModelServicesMain { cols := []table.Column{ {Title: "ID", Width: 0}, // Hidden unique ID {Title: "T", Width: 1}, // Type (Client/Server) @@ -21,42 +22,47 @@ func NewServicesMainModel() *modelServicesMain { {Title: "Tick", Width: 4}, } - return &modelServicesMain{ + return &ModelServicesMain{ table: NewTable(cols), } } -func (m *modelServicesMain) Refresh() { +func (m *ModelServicesMain) Refresh() { m.updateTable(nil) } -func (m *modelServicesMain) Update(msg tea.Msg) tea.Cmd { +func (m *ModelServicesMain) Update(msg tea.Msg) tea.Cmd { return m.updateTable(msg) } -func (m *modelServicesMain) View() string { +func (m *ModelServicesMain) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.table.HelpView() } -func (m *modelServicesMain) GetSelectedID() (string, bool, error) { +func (m *ModelServicesMain) GetSelectedID() (string, bool, error) { row := m.table.SelectedRow() if row == nil { return "", false, errEmptyTable } + return row[0], row[1] == "S", nil } -func (m *modelServicesMain) updateTable(msg tea.Msg) (cmd tea.Cmd) { +func (m *ModelServicesMain) updateTable(msg tea.Msg) (cmd tea.Cmd) { rows := []table.Row{} + mon := monitoring.GetMonitoring(monitoring.MonitorClient | monitoring.MonitorServer) for _, client := range mon.Clients { rows = append(rows, clientToRow(client)) } + for _, server := range mon.Servers { rows = append(rows, serverToRow(server)) } + m.table.SetRows(rows) m.table, cmd = m.table.Update(msg) + return } diff --git a/cmd/monitor/page_topics.go b/cmd/monitor/page_topics.go index 54cbfe6..b6f08bf 100644 --- a/cmd/monitor/page_topics.go +++ b/cmd/monitor/page_topics.go @@ -12,14 +12,14 @@ const ( subpageTopicMessages // TODO: Not implemented ) -type modelTopics struct { +type ModelTopics struct { subpage TopicsPage pages map[TopicsPage]PageModel NavKeys NavKeyMap } -func NewTopicsModel() *modelTopics { - return (&modelTopics{ +func NewTopicsModel() *ModelTopics { + return (&ModelTopics{ subpage: subpageTopicMain, pages: map[TopicsPage]PageModel{ subpageTopicMain: NewTopicsMainModel(), @@ -30,60 +30,69 @@ func NewTopicsModel() *modelTopics { }).Init() } -func (m *modelTopics) navDown() { - switch m.subpage { - case subpageTopicMain: - mainModel := m.pages[subpageTopicMain].(*modelTopicsMain) +func (m *ModelTopics) navDown() tea.Cmd { + if m.subpage == subpageTopicMain { + mainModel := m.pages[subpageTopicMain].(*ModelTopicsMain) + topic, topicType, err := mainModel.GetSelectedID() if err != nil { - return // Don't' transition + return nil // Don't' transition } - detailed := m.pages[subpageTopicDetailed].(*modelTopicDetailed) + + detailed := m.pages[subpageTopicDetailed].(*ModelTopicDetailed) detailed.ShowTopic(topic, topicType) + m.subpage = subpageTopicDetailed } + + return nil } -func (m *modelTopics) navUp() { - switch m.subpage { - default: - m.subpage = subpageTopicMain - } +func (m *ModelTopics) navUp() tea.Cmd { + m.subpage = subpageTopicMain + return nil } -func (m *modelTopics) navMessages() tea.Cmd { +func (m *ModelTopics) navMessages() tea.Cmd { if m.subpage != subpageTopicMain { return nil } - mainModel := m.pages[subpageTopicMain].(*modelTopicsMain) + + mainModel := m.pages[subpageTopicMain].(*ModelTopicsMain) + topic, topicType, err := mainModel.GetSelectedID() if err != nil { return nil // Don't' transition } - messagesModel := m.pages[subpageTopicMessages].(*modelTopicMessages) + + messagesModel := m.pages[subpageTopicMessages].(*ModelTopicMessages) messagesModel.ShowTopic(topic, topicType) + m.subpage = subpageTopicMessages + return messagesModel.Init() } -func (m *modelTopics) Refresh() { +func (m *ModelTopics) Refresh() { m.pages[m.subpage].Refresh() } -func (m *modelTopics) Init() *modelTopics { - m.NavKeys["esc"] = func() tea.Cmd { m.navUp(); return nil } - m.NavKeys["enter"] = func() tea.Cmd { m.navDown(); return nil } +func (m *ModelTopics) Init() *ModelTopics { + m.NavKeys["esc"] = func() tea.Cmd { return m.navUp() } + m.NavKeys["enter"] = func() tea.Cmd { return m.navDown() } m.NavKeys["m"] = func() tea.Cmd { return m.navMessages() } + return m } -func (m *modelTopics) Update(msg tea.Msg) tea.Cmd { +func (m *ModelTopics) Update(msg tea.Msg) tea.Cmd { if cmd, navigated := m.NavKeys.HandleMsg(msg); navigated { return cmd } + return m.pages[m.subpage].Update(msg) } -func (m *modelTopics) View() string { +func (m *ModelTopics) View() string { return m.pages[m.subpage].View() } diff --git a/cmd/monitor/page_topics_detailed.go b/cmd/monitor/page_topics_detailed.go index ffc8004..1cff566 100644 --- a/cmd/monitor/page_topics_detailed.go +++ b/cmd/monitor/page_topics_detailed.go @@ -8,51 +8,51 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type modelTopicDetailed struct { +type ModelTopicDetailed struct { table table.Model id string `exhaustruct:"optional"` - topicType topicType `exhaustruct:"optional"` + topicType TopicType `exhaustruct:"optional"` } -func NewDetailedModel() *modelTopicDetailed { +func NewDetailedModel() *ModelTopicDetailed { cols := []table.Column{ {Title: "", Width: 14}, {Title: "", Width: 67}, } - return &modelTopicDetailed{ + return &ModelTopicDetailed{ table: NewTable(cols), } } -func (m *modelTopicDetailed) ShowTopic(topicID string, topicType topicType) { +func (m *ModelTopicDetailed) ShowTopic(topicID string, topicType TopicType) { m.id = topicID m.topicType = topicType m.updateDetailedTable(nil) } -func (m *modelTopicDetailed) Init() tea.Cmd { +func (m *ModelTopicDetailed) Init() tea.Cmd { return nil } -func (m *modelTopicDetailed) Update(msg tea.Msg) tea.Cmd { +func (m *ModelTopicDetailed) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: + if msg, ok := msg.(tea.KeyMsg); ok { m.table, cmd = m.table.Update(msg) } + return cmd } -func (m *modelTopicDetailed) View() string { +func (m *ModelTopicDetailed) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.table.HelpView() } -func (m *modelTopicDetailed) Refresh() { +func (m *ModelTopicDetailed) Refresh() { m.updateDetailedTable(nil) } -func (m *modelTopicDetailed) updateDetailedTable(msg tea.Msg) { +func (m *ModelTopicDetailed) updateDetailedTable(msg tea.Msg) { t, _ := getTopicFromID(m.topicType, m.id) m.table.Columns()[0].Title = t.Direction m.table.Columns()[1].Title = t.TopicName diff --git a/cmd/monitor/page_topics_main.go b/cmd/monitor/page_topics_main.go index 7def180..b12cd96 100644 --- a/cmd/monitor/page_topics_main.go +++ b/cmd/monitor/page_topics_main.go @@ -4,12 +4,12 @@ import ( "strconv" "strings" - "github.com/DownerCase/ecal-go/ecal/monitoring" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + + "github.com/DownerCase/ecal-go/ecal/monitoring" ) type topicsKeyMap struct { @@ -63,14 +63,14 @@ func (km topicsKeyMap) FullHelp() [][]key.Binding { return append([][]key.Binding{{km.FilterAll, km.FilterPub, km.FilterSub}}, km.KeyMap.FullHelp()...) } -type modelTopicsMain struct { +type ModelTopicsMain struct { table table.Model keymap topicsKeyMap help help.Model filter entityFilter } -func NewTopicsMainModel() *modelTopicsMain { +func NewTopicsMainModel() *ModelTopicsMain { cols := []table.Column{ {Title: "ID", Width: 0}, // Zero width ID column to use as identifier {Title: "D", Width: 1}, @@ -82,18 +82,18 @@ func NewTopicsMainModel() *modelTopicsMain { {Title: "Tick", Width: 4}, } - return &modelTopicsMain{ + return &ModelTopicsMain{ table: NewTable(cols), keymap: newTopicsKeyMap(), help: help.New(), } } -func (m *modelTopicsMain) Refresh() { +func (m *ModelTopicsMain) Refresh() { m.updateTopicsTable(nil) } -func (m *modelTopicsMain) Update(msg tea.Msg) tea.Cmd { +func (m *ModelTopicsMain) Update(msg tea.Msg) tea.Cmd { switch msg := msg.(type) { case tea.KeyMsg: switch { @@ -114,31 +114,36 @@ func (m *modelTopicsMain) Update(msg tea.Msg) tea.Cmd { default: m.updateTopicsTable(msg) } + return nil } -func (m *modelTopicsMain) View() string { +func (m *ModelTopicsMain) View() string { return baseStyle.Render(m.table.View()) + "\n" + m.help.View(m.keymap) } -func (m *modelTopicsMain) GetSelectedID() (string, topicType, error) { +func (m *ModelTopicsMain) GetSelectedID() (string, TopicType, error) { row := m.table.SelectedRow() if row == nil { return "", 0, errEmptyTable } - var topicType topicType + + var topicType TopicType + switch row[1] { case "S": topicType = topicTypeSubscriber case "P": topicType = topicTypePublisher } + return row[0], topicType, nil } -func (m *modelTopicsMain) updateTopicsTable(msg tea.Msg) { +func (m *ModelTopicsMain) updateTopicsTable(msg tea.Msg) { rows := []table.Row{} entities := monitoring.MonitorNone + switch m.filter { case entityAll: entities = monitoring.MonitorPublisher | monitoring.MonitorSubscriber @@ -147,13 +152,16 @@ func (m *modelTopicsMain) updateTopicsTable(msg tea.Msg) { case entityPublisher: entities = monitoring.MonitorPublisher } + mon := monitoring.GetMonitoring(entities) for _, topic := range mon.Publishers { rows = append(rows, topicToRow(topic)) } + for _, topic := range mon.Subscribers { rows = append(rows, topicToRow(topic)) } + m.table.SetRows(rows) m.table, _ = m.table.Update(msg) } diff --git a/cmd/monitor/page_topics_messages.go b/cmd/monitor/page_topics_messages.go index 221e6c5..f3c80f0 100644 --- a/cmd/monitor/page_topics_messages.go +++ b/cmd/monitor/page_topics_messages.go @@ -6,18 +6,18 @@ import ( "strconv" "strings" - "github.com/DownerCase/ecal-go/ecal/monitoring" - "github.com/DownerCase/ecal-go/ecal/subscriber" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/muesli/reflow/wrap" + + "github.com/DownerCase/ecal-go/ecal/monitoring" + "github.com/DownerCase/ecal-go/ecal/subscriber" ) -type modelTopicMessages struct { +type ModelTopicMessages struct { viewport viewport.Model mon monitoring.TopicMon - topicType topicType + topicType TopicType topicID string subscriber *subscriber.Subscriber msg []byte @@ -28,21 +28,23 @@ type msgMsg struct { msg []byte } -func NewTopicsMessagesModel() *modelTopicMessages { +func NewTopicsMessagesModel() *ModelTopicMessages { viewport := viewport.New(85, 10) subscriber, _ := subscriber.New() - return &modelTopicMessages{ + + return &ModelTopicMessages{ viewport: viewport, subscriber: subscriber, } } -func (m *modelTopicMessages) Init() tea.Cmd { +func (m *ModelTopicMessages) Init() tea.Cmd { return m.receiveTicks() } -func (m *modelTopicMessages) Update(msg tea.Msg) tea.Cmd { +func (m *ModelTopicMessages) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd + switch msg := msg.(type) { case msgMsg: m.msg = msg.msg @@ -50,10 +52,11 @@ func (m *modelTopicMessages) Update(msg tea.Msg) tea.Cmd { default: m.viewport, cmd = m.viewport.Update(msg) } + return cmd } -func (m *modelTopicMessages) View() string { +func (m *ModelTopicMessages) View() string { s := strings.Builder{} s.WriteString(highlight.Render(m.mon.TopicName)) s.WriteString( @@ -67,52 +70,58 @@ func (m *modelTopicMessages) View() string { // that we can scroll against m.viewport.SetContent(wrap.String(m.deserializer(m.msg), m.viewport.Width)) s.WriteString(m.viewport.View()) + return baseStyle.Render(s.String()) } -func (m *modelTopicMessages) Refresh() { +func (m *ModelTopicMessages) Refresh() { m.mon, _ = getTopicFromID(m.topicType, m.topicID) } -func (m *modelTopicMessages) ShowTopic(topicID string, topicType topicType) { +func (m *ModelTopicMessages) ShowTopic(topicID string, topicType TopicType) { if m.topicID != topicID { m.topicType = topicType m.topicID = topicID m.mon, _ = getTopicFromID(m.topicType, m.topicID) m.createSubscriber() } + m.Refresh() } -func (m *modelTopicMessages) createSubscriber() { +func (m *ModelTopicMessages) createSubscriber() { // (re)create subscriber with new topic type m.subscriber.Delete() + subscriber, err := subscriber.New() if err != nil { subscriber.Delete() panic(fmt.Errorf("[Topic Messages]: %w", err)) } + err = subscriber.Create(m.mon.TopicName, m.mon.Datatype) if err != nil { subscriber.Delete() panic(fmt.Errorf("[Topic Messages]: %w", err)) } + switch { case m.mon.Datatype.Name == "std::string" && m.mon.Datatype.Encoding == "base": m.deserializer = deserializeBasicString default: m.deserializer = deserializeAsHex } + m.msg = nil m.subscriber = subscriber } -func (m *modelTopicMessages) receiveTicks() tea.Cmd { +func (m *ModelTopicMessages) receiveTicks() tea.Cmd { return func() tea.Msg { - switch msg := (<-m.subscriber.Messages).(type) { - case []byte: + if msg, ok := (<-m.subscriber.Messages).([]byte); ok { return msgMsg{msg: msg} } + return nil } } diff --git a/cmd/monitor/tea.go b/cmd/monitor/tea.go index 8f01ab8..2db34f6 100644 --- a/cmd/monitor/tea.go +++ b/cmd/monitor/tea.go @@ -10,7 +10,7 @@ import ( type PageModel interface { Refresh() - Update(tea.Msg) tea.Cmd + Update(msg tea.Msg) tea.Cmd View() string } @@ -40,6 +40,7 @@ func newModel() *model { pagesMap[pageLogs] = NewLogsModel() pagesMap[pageSystem] = NewConfigModel() pagesMap[pageAbout] = &PlaceholderModel{"About Placeholder"} + return &model{ page: pageTopics, pages: pagesMap, @@ -73,9 +74,11 @@ func (m *model) Init() tea.Cmd { func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd + switch msg := msg.(type) { case TickMsg: m.refresh() + cmd = doTick() case tea.KeyMsg: switch msg.String() { @@ -101,12 +104,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { default: cmd = m.updatePage(msg) } + return m, cmd } func (m *model) View() string { s := strings.Builder{} s.WriteString(m.pages[m.page].View()) + s.WriteString("\n") + tabs := []string{ "1: Topics", "2: Services", @@ -116,13 +122,13 @@ func (m *model) View() string { "6: Config", "7: About", } - s.WriteString("\n") - page := m.page - tabs[page] = highlight.Render(tabs[page]) + tabs[m.page] = highlight.Render(tabs[m.page]) + for _, tab := range tabs { s.WriteString(tab) s.WriteRune(' ') } + return s.String() } diff --git a/ecal/core_test.go b/ecal/core_test.go index fe68479..12c4239 100644 --- a/ecal/core_test.go +++ b/ecal/core_test.go @@ -5,22 +5,30 @@ import ( ) func TestVersionString(t *testing.T) { + t.Parallel() + version := GetVersionString() if version == "" { t.Error("GetVersionString returned empty string") } + t.Log(version) } func TestVersionDateString(t *testing.T) { + t.Parallel() + buildDate := GetVersionDateString() if buildDate == "" { t.Error("GetVersionDateString returned empty string") } + t.Log(buildDate) } func TestGetVersion(t *testing.T) { + t.Parallel() + version := GetVersion() t.Log(version) } @@ -46,6 +54,7 @@ func TestInitializeFinalize(t *testing.T) { if !IsInitialized() { t.Error("IsInitialized return false, expected true") } + if !IsComponentInitialized(CPublisher) { t.Error("Expected publisheCPublisher to be initialised") } diff --git a/ecal/logging/logging_test.go b/ecal/logging/logging_test.go index 3bae1aa..cc1f76d 100644 --- a/ecal/logging/logging_test.go +++ b/ecal/logging/logging_test.go @@ -11,27 +11,35 @@ import ( ) func expectMessageIsFromHost(t *testing.T, msg LogMessage) { + t.Helper() + host, err := os.Hostname() if err != nil && msg.Host != host { t.Error("Host mismatch", host, "!=", msg.Host) } + if int(msg.Pid) != os.Getpid() { t.Error("Mismatch pid", os.Getpid(), "!=", msg.Pid) } } func receiveMessage(t *testing.T, msg string, level Level) bool { + t.Helper() + logs := GetLogging() for _, rmsg := range logs.Messages { if rmsg.Content == msg { expectMessageIsFromHost(t, rmsg) + if rmsg.Level != level { t.Error("Mismatch log level", rmsg.Level, "!=", level) } + return true } } + return false } @@ -47,6 +55,7 @@ func TestGetLogging(t *testing.T) { // Expect: To receieve that message time.Sleep(5 * time.Millisecond) + if !receiveMessage(t, testMessage, level) { t.Error("Could not find expected message:", testMessage) } diff --git a/ecal/monitoring/monitoring_test.go b/ecal/monitoring/monitoring_test.go index 88b6140..b573e8f 100644 --- a/ecal/monitoring/monitoring_test.go +++ b/ecal/monitoring/monitoring_test.go @@ -14,19 +14,24 @@ import ( ) func expectTopicPresent(t *testing.T, ts []TopicMon, topicName string) { + t.Helper() + if len(ts) == 0 { t.Error("Monitoring returned no topics") } + for _, topic := range ts { if topic.TopicName == topicName { return } } + t.Error("Monitoring does not contain expected topic", topicName, "\nReceived", ts) } func TestPublisherMonitoring(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() topic := "test_mon_pub" @@ -50,23 +55,29 @@ func TestPublisherMonitoring(t *testing.T) { } func expectPid(t *testing.T, pid int, procs []ProcessMon) { + t.Helper() + hostname, err := os.Hostname() if err != nil { t.Error("Could not get hostname") } + for _, proc := range procs { if pid == int(proc.Pid) { if proc.HostName != hostname { t.Error("Expected hostname", hostname, "got", proc.HostName) } + return } } + t.Error("Could not find self in process list") } func TestSubscriberMonitoring(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() topic := "test_mon_sub" @@ -92,7 +103,9 @@ func TestSubscriberMonitoring(t *testing.T) { func TestProcessMonitoring(t *testing.T) { // Given: eCAL Initialized ecaltest.InitEcal(t) + defer ecal.Finalize() + time.Sleep(1500 * time.Millisecond) // Propagation delay... // When: Requesting the processes diff --git a/ecal/protobuf/publisher/protobuf_publisher.go b/ecal/protobuf/publisher/protobuf_publisher.go index 9aa00e6..c0c724b 100644 --- a/ecal/protobuf/publisher/protobuf_publisher.go +++ b/ecal/protobuf/publisher/protobuf_publisher.go @@ -1,10 +1,13 @@ package publisher import ( - "github.com/DownerCase/ecal-go/ecal/publisher" - "github.com/DownerCase/ecal-go/internal/protobuf" + "fmt" + "reflect" "google.golang.org/protobuf/proto" + + "github.com/DownerCase/ecal-go/ecal/publisher" + "github.com/DownerCase/ecal-go/internal/protobuf" ) // Type must be a pointer and implement the proto.Message interface. @@ -19,26 +22,37 @@ type Publisher[T proto.Message] struct { func New[U any, T Msg[U]]() (*Publisher[T], error) { pub, err := publisher.New() + if err != nil { + err = fmt.Errorf("protobuf Publisher[%v].New(): %w", reflect.TypeFor[T](), err) + } + return &Publisher[T]{*pub}, err } func (p *Publisher[T]) Send(t T) error { msg, err := proto.Marshal(t) if err != nil { - return err + return fmt.Errorf("protobuf Publisher[%v].Send(): %w", reflect.TypeFor[T](), err) } p.Messages <- msg + return nil } func (p *Publisher[T]) Create(topic string) error { var msg T - return p.Publisher.Create(topic, + + err := p.Publisher.Create(topic, publisher.DataType{ Name: protobuf.GetFullName(msg), Encoding: "proto", Descriptor: protobuf.GetProtoMessageDescription(msg), }, ) + if err != nil { + err = fmt.Errorf("protobuf Publisher[%v].Create(): %w", reflect.TypeFor[T](), err) + } + + return err } diff --git a/ecal/protobuf/subscriber/protobuf_subscriber.go b/ecal/protobuf/subscriber/protobuf_subscriber.go index 09e5b57..4e4d85e 100644 --- a/ecal/protobuf/subscriber/protobuf_subscriber.go +++ b/ecal/protobuf/subscriber/protobuf_subscriber.go @@ -1,16 +1,17 @@ package subscriber import "C" + import ( "fmt" "reflect" "time" "unsafe" + "google.golang.org/protobuf/proto" + "github.com/DownerCase/ecal-go/ecal/subscriber" "github.com/DownerCase/ecal-go/internal/protobuf" - - "google.golang.org/protobuf/proto" ) // Type must be a pointer and implement the proto.Message interface @@ -29,46 +30,53 @@ func New[U any, T Msg[U]]() (*Subscriber[U, T], error) { sub, err := subscriber.New() sub.Deserialize = deserialize[U, T] psub := &Subscriber[U, T]{*sub} + if err != nil { + err = fmt.Errorf("protobuf Subscriber[%v].New(): %w", reflect.TypeFor[T](), err) + } return psub, err } -func (p *Subscriber[U, T]) Receive(timeout time.Duration) (U, error) { +func (s *Subscriber[U, T]) Receive(timeout time.Duration) (U, error) { var u U var msg any select { - case msg = <-p.Messages: + case msg = <-s.Messages: case <-time.After(timeout): return u, fmt.Errorf("[Receive[%v]()]: %w", reflect.TypeFor[U](), subscriber.ErrRcvTimeout) } switch msg := msg.(type) { - case U: - return msg, nil case error: return u, msg + case U: + return msg, nil default: return u, fmt.Errorf("%w: %v", subscriber.ErrRcvBadType, reflect.TypeOf(msg)) } } -func deserialize[U any, T Msg[U]](data unsafe.Pointer, len int) any { +func deserialize[U any, T Msg[U]](data unsafe.Pointer, dataLen int) any { // WARNING: Creates a Go slice backed by C data and deserializes into a Go // value which gets put into the channel - bytesUnsafe := unsafe.Slice((*byte)(data), len) + bytesUnsafe := unsafe.Slice((*byte)(data), dataLen) var msg U err := proto.Unmarshal(bytesUnsafe, T(&msg)) if err != nil { - return err + return fmt.Errorf("protobuf Subscriber[%v].deserialize(): %w", reflect.TypeFor[T](), err) } return msg } func (s *Subscriber[U, T]) Create(topic string) error { var msg T - return s.Subscriber.Create(topic, + err := s.Subscriber.Create(topic, subscriber.DataType{ Name: protobuf.GetFullName(msg), Encoding: "proto", Descriptor: protobuf.GetProtoMessageDescription(msg), }, ) + if err != nil { + err = fmt.Errorf("protobuf Subscriber[%v].Create(): %w", reflect.TypeFor[T](), err) + } + return err } diff --git a/ecal/protobuf/subscriber/protobuf_subscriber_test.go b/ecal/protobuf/subscriber/protobuf_subscriber_test.go index 4c81283..bcdb054 100644 --- a/ecal/protobuf/subscriber/protobuf_subscriber_test.go +++ b/ecal/protobuf/subscriber/protobuf_subscriber_test.go @@ -12,18 +12,23 @@ import ( ) func newSubscriber[U any, T Msg[U]](t *testing.T, topic string) *Subscriber[U, T] { + t.Helper() + sub, err := New[U, T]() if err != nil { t.Error(err) } + if err := sub.Create(topic); err != nil { t.Error(err) } + return sub } func TestSubscriber(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() // Shutdown eCAL at the end of the program pub := testutilpublisher.NewProtobufPublisher[protos.Person](t, "testing_protobuf_subscriber") @@ -33,20 +38,25 @@ func TestSubscriber(t *testing.T) { defer sub.Delete() go sendMessages(pub) + for range 10 { msg, err := sub.Receive(2 * time.Second) if err != nil { t.Error(err) } + if msg.GetId() != 0 { t.Error("Wrong ID") } + if msg.GetName() != "John" { t.Error("Wrong name") } + if msg.GetEmail() != "john@doe.net" { t.Error("Wrong email") } + if msg.GetDog().GetName() != "Pluto" { t.Error("Wrong dog") } @@ -55,9 +65,12 @@ func TestSubscriber(t *testing.T) { func TestSubscriberTimeout(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() // Shutdown eCAL at the end of the program + sub := newSubscriber[protos.Person](t, "testing_protobuf_subscriber_timeout") defer sub.Delete() + msg, err := sub.Receive(50 * time.Millisecond) if err == nil { t.Error("Expected timeout, received message:", &msg) @@ -73,7 +86,8 @@ func sendMessages(p *publisher.Publisher[*protos.Person]) { for !p.IsStopped() { _ = p.Send(person) - person.House.Rooms += 1 + person.House.Rooms++ + time.Sleep(10 * time.Millisecond) } } diff --git a/ecal/publisher/cgo_wrapping.go b/ecal/publisher/cgo_wrapping.go index a025a29..fb8f927 100644 --- a/ecal/publisher/cgo_wrapping.go +++ b/ecal/publisher/cgo_wrapping.go @@ -2,12 +2,12 @@ package publisher // #include "publisher.h" -//bool GoPublisherCreate( +// bool GoPublisherCreate( // uintptr_t handle, // _GoString_ topic, // _GoString_ name, _GoString_ encoding, // const char* const descriptor, size_t descriptor_len -//) { +// ) { // return PublisherCreate( // handle, // _GoStringPtr(topic), _GoStringLen(topic), diff --git a/ecal/publisher/publisher.go b/ecal/publisher/publisher.go index 0396102..196dd58 100644 --- a/ecal/publisher/publisher.go +++ b/ecal/publisher/publisher.go @@ -3,12 +3,12 @@ package publisher // #cgo LDFLAGS: -lecal_core // #cgo CPPFLAGS: -I${SRCDIR}/../../ //#include "publisher.h" -//bool GoPublisherCreate( +// bool GoPublisherCreate( // uintptr_t handle, // _GoString_ topic, // _GoString_ name, _GoString_ encoding, // const char* const descriptor, size_t descriptor_len -//); +// ); import "C" import ( @@ -60,7 +60,7 @@ func (p *Publisher) Delete() { } func (p *Publisher) Create(topic string, datatype DataType) error { - var descriptorPtr *C.char = nil + var descriptorPtr *C.char if len(datatype.Descriptor) > 0 { descriptorPtr = (*C.char)(unsafe.Pointer(&datatype.Descriptor[0])) } diff --git a/ecal/publisher/publisher_test.go b/ecal/publisher/publisher_test.go index 8d5b9cf..32b1873 100644 --- a/ecal/publisher/publisher_test.go +++ b/ecal/publisher/publisher_test.go @@ -20,6 +20,7 @@ func TestNewPublishers(t *testing.T) { func TestPublisher(t *testing.T) { pub := testutilpublisher.NewGenericPublisher(t, "testing") defer pub.Delete() + if pub.Messages == nil { t.Error("Message channel nil") } diff --git a/ecal/registration/registration_test.go b/ecal/registration/registration_test.go index 1af96fc..edf74e8 100644 --- a/ecal/registration/registration_test.go +++ b/ecal/registration/registration_test.go @@ -13,6 +13,7 @@ import ( func TestPublisherCallback(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() topic := "test_reg_pub" @@ -32,6 +33,7 @@ func TestPublisherCallback(t *testing.T) { func TestSubscriberCallback(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() topic := "test_reg_sub" diff --git a/ecal/string/publisher/string_publisher.go b/ecal/string/publisher/string_publisher.go index 90c1dd3..b792e88 100644 --- a/ecal/string/publisher/string_publisher.go +++ b/ecal/string/publisher/string_publisher.go @@ -12,6 +12,10 @@ type Publisher struct { func New() (*Publisher, error) { pub, err := publisher.New() + if err != nil { + err = fmt.Errorf("string Publisher.New(): %w", err) + } + return &Publisher{*pub}, err } @@ -28,10 +32,15 @@ func (p *Publisher) Sendf(format string, a ...any) error { } func (p *Publisher) Create(topic string) error { - return p.Publisher.Create(topic, + err := p.Publisher.Create(topic, publisher.DataType{ Name: "std::string", Encoding: "base", }, ) + if err != nil { + err = fmt.Errorf("string Publisher.Create(): %w", err) + } + + return err } diff --git a/ecal/string/subscriber/string_subscriber.go b/ecal/string/subscriber/string_subscriber.go index 0a15751..98c50d7 100644 --- a/ecal/string/subscriber/string_subscriber.go +++ b/ecal/string/subscriber/string_subscriber.go @@ -1,6 +1,7 @@ package subscriber import "C" + import ( "fmt" "time" @@ -16,27 +17,34 @@ type Subscriber struct { func New() (*Subscriber, error) { sub, err := subscriber.New() sub.Deserialize = deserialize + if err != nil { + err = fmt.Errorf("string Subscriber.New(): %w", err) + } return &Subscriber{*sub}, err } -func (p *Subscriber) Receive(timeout time.Duration) (string, error) { +func (s *Subscriber) Receive(timeout time.Duration) (string, error) { select { - case msg := <-p.Messages: + case msg := <-s.Messages: return msg.(string), nil case <-time.After(timeout): return "", fmt.Errorf("[Receive]: %w", subscriber.ErrRcvTimeout) } } -func deserialize(data unsafe.Pointer, len int) any { - return C.GoStringN((*C.char)(data), C.int(len)) +func deserialize(data unsafe.Pointer, dataLen int) any { + return C.GoStringN((*C.char)(data), C.int(dataLen)) } func (s *Subscriber) Create(topic string) error { - return s.Subscriber.Create(topic, + err := s.Subscriber.Create(topic, subscriber.DataType{ Name: "std::string", Encoding: "base", }, ) + if err != nil { + err = fmt.Errorf("string Subscriber.Create(): %w", err) + } + return err } diff --git a/ecal/string/subscriber/string_subscriber_test.go b/ecal/string/subscriber/string_subscriber_test.go index 0d9f8a9..7b45d48 100644 --- a/ecal/string/subscriber/string_subscriber_test.go +++ b/ecal/string/subscriber/string_subscriber_test.go @@ -14,13 +14,17 @@ import ( const TestMessage = "Test string" func newSubscriber(t *testing.T, topic string) *Subscriber { + t.Helper() + sub, err := New() if err != nil { t.Error(err) } + if err := sub.Create(topic); err != nil { t.Error(err) } + return sub } @@ -29,6 +33,7 @@ var NewSubscriber = newSubscriber func TestSubscriber(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() // Shutdown eCAL at the end of the program pub := testutilpublisher.NewStringPublisher(t, "testing_string_subscriber") @@ -38,14 +43,17 @@ func TestSubscriber(t *testing.T) { defer sub.Delete() go sendMessages(pub) + for range 10 { msg, err := sub.Receive(2 * time.Second) if err != nil { t.Error(err) } + if len(msg) != len(TestMessage) { t.Error("Expected message of length", len(TestMessage), "Received:", len(msg)) } + if !reflect.DeepEqual(msg, TestMessage) { t.Error(msg, "!=", TestMessage) } @@ -54,9 +62,12 @@ func TestSubscriber(t *testing.T) { func TestSubscriberTimeout(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() // Shutdown eCAL at the end of the program + sub := newSubscriber(t, "testing_string_subscriber_timeout") defer sub.Delete() + msg, err := sub.Receive(50 * time.Millisecond) if err == nil { t.Error("Expected timeout, received message:", msg) @@ -66,6 +77,7 @@ func TestSubscriberTimeout(t *testing.T) { func sendMessages(p *publisher.Publisher) { for !p.IsStopped() { p.Messages <- []byte(TestMessage) + time.Sleep(10 * time.Millisecond) } } diff --git a/ecal/subscriber/cgo_wrapping.go b/ecal/subscriber/cgo_wrapping.go index 2cd4aeb..0a3afaa 100644 --- a/ecal/subscriber/cgo_wrapping.go +++ b/ecal/subscriber/cgo_wrapping.go @@ -2,7 +2,7 @@ package subscriber //#include "subscriber.h" -//bool GoSubscriberCreate( +// bool GoSubscriberCreate( // uintptr_t handle, // _GoString_ topic, // _GoString_ name, _GoString_ encoding, diff --git a/ecal/subscriber/subscriber.go b/ecal/subscriber/subscriber.go index 7dd243e..8d82e5a 100644 --- a/ecal/subscriber/subscriber.go +++ b/ecal/subscriber/subscriber.go @@ -66,7 +66,7 @@ func (p *Subscriber) Delete() { } func (p *Subscriber) Create(topic string, datatype DataType) error { - var descriptorPtr *C.char = nil + var descriptorPtr *C.char if len(datatype.Descriptor) > 0 { descriptorPtr = (*C.char)(unsafe.Pointer(&datatype.Descriptor[0])) } @@ -94,8 +94,8 @@ func (p *Subscriber) Receive(timeout time.Duration) ([]byte, error) { } // Deserialize straight from the eCAL internal buffer to our Go []byte -func deserializer(data unsafe.Pointer, len int) any { - return C.GoBytes(data, C.int(len)) +func deserializer(data unsafe.Pointer, dataLen int) any { + return C.GoBytes(data, C.int(dataLen)) } // This function is called by the C code whenever a new message is received @@ -103,11 +103,11 @@ func deserializer(data unsafe.Pointer, len int) any { // If the subscriber Receive is not waiting the incoming message will be dropped // //export goReceiveCallback -func goReceiveCallback(handle C.uintptr_t, data unsafe.Pointer, len C.long) { +func goReceiveCallback(handle C.uintptr_t, data unsafe.Pointer, dataLen C.long) { h := cgo.Handle(handle) sub := h.Value().(*Subscriber) select { - case sub.Messages <- sub.Deserialize(data, int(len)): + case sub.Messages <- sub.Deserialize(data, int(dataLen)): default: } } diff --git a/ecal/subscriber/subscriber_test.go b/ecal/subscriber/subscriber_test.go index 4791fbd..10d8d4c 100644 --- a/ecal/subscriber/subscriber_test.go +++ b/ecal/subscriber/subscriber_test.go @@ -16,6 +16,7 @@ var TestMessage = []byte{4, 15, 80} func TestSubscriber(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() // Shutdown eCAL at the end of the program pub := testutilpublisher.NewGenericPublisher(t, "testing_subscriber") @@ -25,6 +26,7 @@ func TestSubscriber(t *testing.T) { defer sub.Delete() go sendMessages(pub) + for range 10 { // TODO: Reduce the propagation delay for when the subscriber gets // connected to the publisher @@ -32,12 +34,15 @@ func TestSubscriber(t *testing.T) { if err != nil { t.Error("Received err:", err) } + if msg == nil { t.Error("Nil message received:") } + if len(msg) != len(TestMessage) { t.Error("Expected message of length", len(TestMessage), "Received:", len(msg)) } + if !reflect.DeepEqual(msg, TestMessage) { t.Error(msg, "!=", TestMessage) } @@ -46,9 +51,12 @@ func TestSubscriber(t *testing.T) { func TestSubscriberTimeout(t *testing.T) { ecaltest.InitEcal(t) + defer ecal.Finalize() // Shutdown eCAL at the end of the program + sub := testutilsubscriber.NewGenericSubscriber(t, "testing_subscriber_timeout") defer sub.Delete() + msg, err := sub.Receive(50 * time.Millisecond) if err == nil { t.Error("Expected timeout, received message:", msg) @@ -58,6 +66,7 @@ func TestSubscriberTimeout(t *testing.T) { func sendMessages(p *publisher.Publisher) { for !p.IsStopped() { p.Messages <- TestMessage + time.Sleep(10 * time.Millisecond) } } diff --git a/internal/ecaltest/init.go b/internal/ecaltest/init.go index bd5cc3a..222639f 100644 --- a/internal/ecaltest/init.go +++ b/internal/ecaltest/init.go @@ -7,6 +7,8 @@ import ( ) func InitEcal(t *testing.T, opts ...ecal.ConfigOption) { + t.Helper() + initResult := ecal.Initialize( ecal.NewConfig(opts...), "Go eCAL!", diff --git a/internal/ecaltest/protobuf/testutil_publisher/testutil_protobuf_publisher.go b/internal/ecaltest/protobuf/testutil_publisher/testutil_protobuf_publisher.go index 3402add..8e44f5b 100644 --- a/internal/ecaltest/protobuf/testutil_publisher/testutil_protobuf_publisher.go +++ b/internal/ecaltest/protobuf/testutil_publisher/testutil_protobuf_publisher.go @@ -7,6 +7,8 @@ import ( ) func NewProtobufPublisher[U any, T publisher.Msg[U]](t *testing.T, topic string) *publisher.Publisher[T] { + t.Helper() + pub, err := publisher.New[U, T]() if err != nil { t.Error(err) @@ -15,5 +17,6 @@ func NewProtobufPublisher[U any, T publisher.Msg[U]](t *testing.T, topic string) if err := pub.Create(topic); err != nil { t.Error(err) } + return pub } diff --git a/internal/ecaltest/regtest/test_registration.go b/internal/ecaltest/regtest/test_registration.go index a9b993e..4143a5c 100644 --- a/internal/ecaltest/regtest/test_registration.go +++ b/internal/ecaltest/regtest/test_registration.go @@ -7,6 +7,11 @@ import ( "github.com/DownerCase/ecal-go/ecal/registration" ) +const ( + RegistrationTimeout = 3 * time.Second + SynchronizationDelay = 50 * time.Millisecond +) + type Callback struct { Event registration.Event ID registration.TopicID @@ -20,28 +25,34 @@ func EventCallback(topic string, channel chan Callback) func(registration.TopicI } } -func expectEvent(event registration.Event, t *testing.T, topic string, channel chan Callback) { +func expectEvent(t *testing.T, event registration.Event, topic string, channel chan Callback) { + t.Helper() + var response Callback select { case response = <-channel: - case <-time.After(3 * time.Second): + case <-time.After(RegistrationTimeout): t.Error("Registration timeout") return } - if response.ID.TopicName != topic { + + switch { + case response.ID.TopicName != topic: // Should be pre-filtered by callback t.Error("Unexpected event for topic", response.ID.TopicName) - } else if response.Event != event { + case response.Event != event: t.Error("Expected event", event, "actual", response.Event) - } else { - time.Sleep(50 * time.Millisecond) // Small delay to allow eCAL to finish + default: + time.Sleep(SynchronizationDelay) // Small delay to allow eCAL to finish } } func ExpectNew(t *testing.T, topic string, channel chan Callback) { - expectEvent(registration.EntityNew, t, topic, channel) + t.Helper() + expectEvent(t, registration.EntityNew, topic, channel) } func ExpectDeleted(t *testing.T, topic string, channel chan Callback) { - expectEvent(registration.EntityDeleted, t, topic, channel) + t.Helper() + expectEvent(t, registration.EntityDeleted, topic, channel) } diff --git a/internal/ecaltest/string/testutil_publisher/testutil_string_publisher.go b/internal/ecaltest/string/testutil_publisher/testutil_string_publisher.go index abe6dac..92a412b 100644 --- a/internal/ecaltest/string/testutil_publisher/testutil_string_publisher.go +++ b/internal/ecaltest/string/testutil_publisher/testutil_string_publisher.go @@ -7,6 +7,8 @@ import ( ) func NewStringPublisher(t *testing.T, topic string) *publisher.Publisher { + t.Helper() + pub, err := publisher.New() if err != nil { t.Error(err) @@ -15,5 +17,6 @@ func NewStringPublisher(t *testing.T, topic string) *publisher.Publisher { if err := pub.Create(topic); err != nil { t.Error(err) } + return pub } diff --git a/internal/ecaltest/testutil_publisher/testutil_publisher.go b/internal/ecaltest/testutil_publisher/testutil_publisher.go index c1dceec..1b2b8d4 100644 --- a/internal/ecaltest/testutil_publisher/testutil_publisher.go +++ b/internal/ecaltest/testutil_publisher/testutil_publisher.go @@ -7,6 +7,8 @@ import ( ) func NewGenericPublisher(t *testing.T, topic string) *publisher.Publisher { + t.Helper() + pub, err := publisher.New() if err != nil { t.Error(err) @@ -15,5 +17,6 @@ func NewGenericPublisher(t *testing.T, topic string) *publisher.Publisher { if err := pub.Create(topic, publisher.DataType{}); err != nil { t.Error(err) } + return pub } diff --git a/internal/ecaltest/testutil_subscriber/testutil_subscriber.go b/internal/ecaltest/testutil_subscriber/testutil_subscriber.go index 56c31bc..8975241 100644 --- a/internal/ecaltest/testutil_subscriber/testutil_subscriber.go +++ b/internal/ecaltest/testutil_subscriber/testutil_subscriber.go @@ -7,12 +7,16 @@ import ( ) func NewGenericSubscriber(t *testing.T, topic string) *subscriber.Subscriber { + t.Helper() + sub, err := subscriber.New() if err != nil { t.Error(err) } + if err := sub.Create(topic, subscriber.DataType{}); err != nil { t.Error(err) } + return sub } diff --git a/internal/protobuf/protobuf.go b/internal/protobuf/protobuf.go index c3b4b4d..42741d5 100644 --- a/internal/protobuf/protobuf.go +++ b/internal/protobuf/protobuf.go @@ -2,6 +2,7 @@ package protobuf import ( "log" + "slices" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" @@ -9,13 +10,11 @@ import ( "google.golang.org/protobuf/types/descriptorpb" ) -func hasFile(fset *descriptorpb.FileDescriptorSet, fname string) bool { - for _, file := range fset.GetFile() { - if file.GetName() == fname { - return true - } - } - return false +func hasFile(fsetFile []*descriptorpb.FileDescriptorProto, fname string) bool { + return slices.ContainsFunc( + fsetFile, + func(f *descriptorpb.FileDescriptorProto) bool { return f.GetName() == fname }, + ) } func getFileDescriptor(desc protoreflect.MessageDescriptor, fset *descriptorpb.FileDescriptorSet) { @@ -26,6 +25,7 @@ func getFileDescriptor(desc protoreflect.MessageDescriptor, fset *descriptorpb.F fdesc := desc.ParentFile() imports := fdesc.Imports() + // TODO: Iterate services after the enums for i := range imports.Len() { sfdesc := imports.Get(i) @@ -36,21 +36,19 @@ func getFileDescriptor(desc protoreflect.MessageDescriptor, fset *descriptorpb.F // Iterate enums if sfdesc.Enums().Len() > 0 { - edesc := sfdesc.Enums().Get(0) - efdesc := edesc.ParentFile() - if !hasFile(fset, efdesc.Path()) { + efdesc := sfdesc.Enums().Get(0).ParentFile() + if !hasFile(fset.GetFile(), efdesc.Path()) { // Add the file to the set fset.File = append(fset.File, protodesc.ToFileDescriptorProto(efdesc)) } } - - // TODO: Iterate services } - if hasFile(fset, fdesc.Path()) { + if hasFile(fset.GetFile(), fdesc.Path()) { // File already added to descriptor set, continue return } + fset.File = append(fset.File, protodesc.ToFileDescriptorProto(fdesc)) // Add fields @@ -63,11 +61,13 @@ func GetProtoMessageDescription(msg proto.Message) []byte { desc := msg.ProtoReflect().Descriptor() pset := descriptorpb.FileDescriptorSet{} getFileDescriptor(desc, &pset) + bytes, err := proto.Marshal(&pset) if err != nil { log.Println("WARN: GetProtoMessageDescription failed to marshal file descriptor set", err) return nil } + return bytes } diff --git a/internal/protobuf/protobuf_test.go b/internal/protobuf/protobuf_test.go index 87dc1ef..51b5ba8 100644 --- a/internal/protobuf/protobuf_test.go +++ b/internal/protobuf/protobuf_test.go @@ -7,6 +7,8 @@ import ( ) func TestFullName(t *testing.T) { + t.Parallel() + expectedName := "pb.People.Person" if fn := GetFullName(&protos.Person{}); fn != expectedName { t.Error("Expected: ", expectedName, " Actual: ", fn) diff --git a/main.go b/main.go index 3013135..d2c9416 100644 --- a/main.go +++ b/main.go @@ -70,9 +70,10 @@ func main() { if sub.Create("string topic") != nil { panic("Failed to Create string subscriber") } + go receiveMessages(sub) - for idx := range 100 { + for idx := range int32(100) { // Check if program has been requested to stop if !ecal.Ok() { logging.Warn("eCAL.Ok() is false; shutting down") @@ -82,7 +83,7 @@ func main() { logging.Info("Sending message ", idx) // Update message to send - person.Id = int32(idx) + person.Id = idx // Serialize and send protobuf message if err := pub.Send(person); err != nil { @@ -109,6 +110,6 @@ func receiveMessages(s *subscriber.Subscriber) { } } -func registrationLogger(id registration.TopicID, event registration.Event) { +func registrationLogger(id registration.TopicID, _ registration.Event) { fmt.Println("Received registration sample:", id) //nolint:forbidigo }