From 5aac795b2c87d769910400380154f576af9d0192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20R=C3=BChl?= Date: Thu, 15 Aug 2024 17:53:02 +0200 Subject: [PATCH] test(plc4go/bacnet): add tests (initial sync) --- .../internal/bacnetip/tests/state_machine.go | 1616 +++++++++++++++++ .../test_max_apdu_length_accepted_test.go | 47 + .../test_base_types/test_name_value_test.go | 22 + .../bacnetip/tests/test_bvll/helpers.go | 293 +++ .../tests/test_bvll/test_simple_test.go | 215 +++ .../tests/test_pdu/test_address_test.go | 649 +++++++ .../bacnetip/tests/test_pdu/test_pci_test.go | 22 + .../bacnetip/tests/test_pdu/test_pdu_test.go | 22 + .../bacnetip/tests/test_primitive_data/todo | 0 .../tests/test_segmentation/test_1_test.go | 558 ++++++ .../bacnetip/tests/test_service/helpers.go | 492 +++++ .../tests/test_service/test_cov_av_test.go | 22 + .../tests/test_service/test_cov_bv_test.go | 22 + .../tests/test_service/test_cov_pc_test.go | 22 + .../tests/test_service/test_cov_test.go | 66 + .../tests/test_service/test_device_2_test.go | 22 + .../tests/test_service/test_device_test.go | 22 + .../tests/test_service/test_file_test.go | 22 + .../tests/test_service/test_object_test.go | 22 + .../test_client_state_machine_test.go | 63 + .../test_server_state_machine_test.go | 63 + .../test_service_access_point_test.go | 247 +++ .../test_utilities/test_state_machine_test.go | 666 +++++++ .../test_utilities/test_time_machine_test.go | 306 ++++ .../tests/test_vlan/test_ipnetwork_test.go | 475 +++++ .../tests/test_vlan/test_network_test.go | 348 ++++ .../internal/bacnetip/tests/time_machine.go | 236 +++ .../bacnetip/tests/trapped_classes.go | 569 ++++++ plc4go/internal/bacnetip/tests/util.go | 67 + 29 files changed, 7196 insertions(+) create mode 100644 plc4go/internal/bacnetip/tests/state_machine.go create mode 100644 plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_bvll/helpers.go create mode 100644 plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_primitive_data/todo create mode 100644 plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/helpers.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_cov_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_device_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_file_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_service/test_object_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go create mode 100644 plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go create mode 100644 plc4go/internal/bacnetip/tests/time_machine.go create mode 100644 plc4go/internal/bacnetip/tests/trapped_classes.go create mode 100644 plc4go/internal/bacnetip/tests/util.go diff --git a/plc4go/internal/bacnetip/tests/state_machine.go b/plc4go/internal/bacnetip/tests/state_machine.go new file mode 100644 index 00000000000..bbfc0db13ae --- /dev/null +++ b/plc4go/internal/bacnetip/tests/state_machine.go @@ -0,0 +1,1616 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tests + +import ( + "bytes" + "fmt" + "reflect" + "slices" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// Transition Instances of this class are transitions betweeen getStates of a state machine. +type Transition struct { + nextState State +} + +func (t Transition) String() string { + return fmt.Sprintf("Transition{nextState: %s}", t.nextState) +} + +type SendTransition struct { + Transition + pdu bacnetip.PDU +} + +func (t SendTransition) String() string { + return fmt.Sprintf("SendTransition{Transition: %s, pdu: %s}", t.Transition, t.pdu) +} + +type criteria struct { + pduType any + pduAttrs map[string]any +} + +func (c criteria) String() string { + return fmt.Sprintf("criteria{%s, %v}", c.pduType, c.pduAttrs) +} + +type ReceiveTransition struct { + Transition + criteria criteria +} + +func (t ReceiveTransition) String() string { + return fmt.Sprintf("ReceiveTransition{Transition: %s, criteria: %s}", t.Transition, t.criteria) +} + +type EventTransition struct { + Transition + eventId string +} + +func (t EventTransition) String() string { + return fmt.Sprintf("EventTransition{Transition: %s, eventId: %s}", t.Transition, t.eventId) +} + +type TimeoutTransition struct { + Transition + timeout time.Time +} + +func (t TimeoutTransition) String() string { + return fmt.Sprintf("TimeoutTransition{Transition: %s, timeout: %s}", t.Transition, t.timeout) +} + +type fnargs struct { + fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error + args bacnetip.Args + kwargs bacnetip.KWArgs +} + +func (f fnargs) String() string { + return fmt.Sprintf("fnargs{fn: %t, args: %s, kwargs: %s}", f.fn == nil, f.args, f.kwargs) +} + +type CallTransition struct { + Transition + fnargs fnargs +} + +func (t CallTransition) String() string { + return fmt.Sprintf("CallTransition{Transition: %s, fnargs: %s}", t.Transition, t.fnargs) +} + +func MatchPdu(localLog zerolog.Logger, pdu bacnetip.PDU, pduType any, pduAttrs map[string]any) bool { + // check the type + if pduType != nil && fmt.Sprintf("%T", pdu) != fmt.Sprintf("%T", pduType) { + localLog.Debug().Msg("failed match, wrong type") + return false + } + for attrName, attrValue := range pduAttrs { + switch attrName { + case "pduSource": + if !pdu.GetPDUSource().Equals(attrValue) { + localLog.Debug().Msg("source doesn't match") + return false + } + case "pduDestination": + if !pdu.GetPDUDestination().Equals(attrValue) { + localLog.Debug().Msg("destination doesn't match") + return false + } + case "x": // only used in test cases + return bytes.Equal(pdu.(interface{ X() []byte }).X(), attrValue.([]byte)) + case "y": // only used in test cases + return false + case "a": // only used in test cases + a := pdu.(interface{ A() int }).A() + if a == 0 { + return false + } + return a == attrValue.(int) + case "b": // only used in test cases + b := pdu.(interface{ B() int }).B() + if b == 0 { + return false + } + return b == attrValue.(int) + case "pduData": + return reflect.DeepEqual(pdu.GetMessage(), attrValue) + default: + panic("implement " + attrName) + } + } + // TODO: implement + /* + # + # check for matching attribute values + for attr_name, attr_value in pdu_attrs.items(): + if not hasattr(pdu, attr_name): + if _debug: matchbacnetip.PDU._debug(" - failed match, missing attr: %r", attr_name) + return False + if getattr(pdu, attr_name) != attr_value: + if _debug: stateMachine._debug(" - failed match, attr value: %r, %r", attr_name, attr_value) + return False + */ + localLog.Trace().Msg("successful match") + return true +} + +type TimeoutTask struct { + *bacnetip.OneShotTask + + fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error + args bacnetip.Args + kwargs bacnetip.KWArgs +} + +func NewTimeoutTask(fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error, args bacnetip.Args, kwargs bacnetip.KWArgs, when *time.Time) *TimeoutTask { + task := &TimeoutTask{ + fn: fn, + args: args, + kwargs: kwargs, + } + task.OneShotTask = bacnetip.NewOneShotTask(task, when) + return task +} + +func (t *TimeoutTask) ProcessTask() error { + return t.fn(t.args, t.kwargs) +} + +func (t *TimeoutTask) String() string { + return fmt.Sprintf("TimeoutTask(%v, fn: %t, args: %s, kwargs: %s)", t.Task, t.fn != nil, t.args, t.kwargs) +} + +type StateInterceptor interface { + BeforeSend(pdu bacnetip.PDU) + AfterSend(pdu bacnetip.PDU) + BeforeReceive(pdu bacnetip.PDU) + AfterReceive(pdu bacnetip.PDU) + UnexpectedReceive(pdu bacnetip.PDU) +} + +type State interface { + fmt.Stringer + + Send(pdu bacnetip.PDU, nextState State) State + Receive(pduType any, pduAttrs map[string]any) State + Reset() + Fail(docstring string) State + Success(docstring string) State + ExitState() + EnterState() + EventSet(eventId string) + Timeout(duration time.Duration, nextState State) State + WaitEvent(eventId string, nextState State) State + SetEvent(eventId string) State + Doc(docstring string) State + DocString() string + Call(fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error, args bacnetip.Args, kwargs bacnetip.KWArgs) State + + getStateMachine() StateMachine + setStateMachine(StateMachine) + getDocString() string + IsSuccessState() bool + IsFailState() bool + getSendTransitions() []SendTransition + getReceiveTransitions() []ReceiveTransition + getSetEventTransitions() []EventTransition + getClearEventTransitions() []EventTransition + getWaitEventTransitions() []EventTransition + getTimeoutTransition() *TimeoutTransition + getCallTransition() *CallTransition + getInterceptor() StateInterceptor + + Equals(other State) bool +} + +type state struct { + interceptor StateInterceptor + + stateMachine StateMachine + docString string + isSuccessState bool + isFailState bool + sendTransitions []SendTransition + receiveTransitions []ReceiveTransition + setEventTransitions []EventTransition + clearEventTransitions []EventTransition + waitEventTransitions []EventTransition + timeoutTransition *TimeoutTransition + callTransition *CallTransition + + log zerolog.Logger +} + +func (s *state) Equals(other State) bool { + if s == other { + return true + } + return false +} + +func (s *state) getStateMachine() StateMachine { + return s.stateMachine +} + +func (s *state) setStateMachine(machine StateMachine) { + s.stateMachine = machine +} + +func (s *state) getDocString() string { + return s.docString +} + +func (s *state) getSendTransitions() []SendTransition { + return s.sendTransitions +} + +func (s *state) getReceiveTransitions() []ReceiveTransition { + return s.receiveTransitions +} + +func (s *state) getSetEventTransitions() []EventTransition { + return s.setEventTransitions +} + +func (s *state) getClearEventTransitions() []EventTransition { + return s.clearEventTransitions +} + +func (s *state) getWaitEventTransitions() []EventTransition { + return s.waitEventTransitions +} + +func (s *state) getTimeoutTransition() *TimeoutTransition { + return s.timeoutTransition +} + +func (s *state) getCallTransition() *CallTransition { + return s.callTransition +} + +func (s *state) getInterceptor() StateInterceptor { + return s.interceptor +} + +func NewState(localLog zerolog.Logger, stateMachine StateMachine, docString string, opts ...func(state *state)) State { + s := &state{ + stateMachine: stateMachine, + docString: docString, + + log: localLog.With().Str("docString", docString).Logger(), + } + for _, opt := range opts { + opt(s) + } + if s.interceptor == nil { + s.interceptor = s + } + return s +} + +func WithStateStateInterceptor(interceptor StateInterceptor) func(state *state) { + return func(state *state) { + state.interceptor = interceptor + } +} + +func (s *state) String() string { + if s == nil { + return "(*state)" + } + return fmt.Sprintf("state(doc: %s, successState: %t, isFailState: %t)", s.docString, s.isSuccessState, s.isFailState) +} + +// Reset Override this method in a derived class if the state maintains counters or other information. Called when the +// +// associated state machine is Reset. +func (s *state) Reset() { + s.log.Trace().Msg("Reset") +} + +// Doc Change the documentation string (label) for the state. The state +// +// is returned for method chaining. +func (s *state) Doc(docString string) State { + s.log.Debug().Str("docString", docString).Msg("Doc") + s.docString = docString + return s +} + +func (s *state) DocString() string { + return s.docString +} + +// Success Mark a state as a successful final state. The state is returned for method chaining. docString: an optional +// +// label for the state +func (s *state) Success(docString string) State { + s.log.Debug().Str("docString", docString).Msg("Success") + if s.isSuccessState { + panic("already a Success state") + } + if s.isFailState { + panic("already a Fail state") + } + + s.isSuccessState = true + + if docString != "" { + s.docString = docString + } else if s.docString == "" { + s.docString = "Success" + } + return s +} + +func (s *state) IsSuccessState() bool { + return s.isSuccessState +} + +// Fail Mark a state as a failure final state. The state is returned for method chaining. docString: an optional +// +// label for the state +func (s *state) Fail(docString string) State { + s.log.Debug().Str("docString", docString).Msg("Fail") + if s.isSuccessState { + panic("already a Success state") + } + if s.isFailState { + panic("already a Fail state") + } + + s.isFailState = true + + if docString != "" { + s.docString = docString + } else if s.docString == "" { + s.docString = "Fail" + } + return s +} + +func (s *state) IsFailState() bool { + return s.isFailState +} + +// EnterState Called when the state machine is entering the state. +func (s *state) EnterState() { + s.log.Debug().Msg("EnterState") + if s.timeoutTransition != nil { + s.log.Debug().Time("timeout", s.timeoutTransition.timeout).Msg("waiting") + s.stateMachine.getStateTimeoutTask().InstallTask(bacnetip.InstallTaskOptions{When: &s.timeoutTransition.timeout}) + } else { + s.log.Trace().Msg("no timeout") + } +} + +// ExitState Called when the state machine is existing the state. +func (s *state) ExitState() { + s.log.Debug().Msg("ExitState") + if s.timeoutTransition != nil { + s.log.Trace().Msg("canceling timeout") + s.stateMachine.getStateTimeoutTask().SuspendTask() + } +} + +// Send Create a SendTransition from this state to another, possibly new, state. The next state is returned for method +// +// chaining. pdu tPDU to send nextState state to transition to after sending +func (s *state) Send(pdu bacnetip.PDU, nextState State) State { + s.log.Debug().Stringer("pdu", pdu).Msg("Send") + if nextState == nil { + nextState = s.stateMachine.NewState("") + s.log.Debug().Stringer("nextState", nextState).Msg("new nextState") + } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) { + panic("off the rails") + } + + s.sendTransitions = append(s.sendTransitions, SendTransition{ + Transition: Transition{nextState: nextState}, + pdu: pdu, + }) + return nextState +} + +// BeforeSend Called before each tPDU about to be sent. +func (s *state) BeforeSend(pdu bacnetip.PDU) { + s.stateMachine.BeforeSend(pdu) +} + +// AfterSend Called after each tPDU about to be sent. +func (s *state) AfterSend(pdu bacnetip.PDU) { + s.stateMachine.AfterSend(pdu) +} + +// Receive Create a ReceiveTransition from this state to another, possibly new, +// +// state. The next state is returned for method chaining. +// +// criteria tPDU to match +// next_state destination state after a successful match +func (s *state) Receive(pduType any, pduAttrs map[string]any) State { + s.log.Debug().Interface("pduType", pduType).Interface("pduAttrs", pduAttrs).Msg("Receive") + var nextState State + if _nextState, ok := pduAttrs["next_state"]; ok { + nextState = _nextState.(State) + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + } + + if nextState == nil { + nextState = s.stateMachine.NewState("") + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) { + panic("off the rails") + } + + // add this to the list of transitions + criteria := criteria{ + pduType: pduType, + pduAttrs: pduAttrs, + } + s.log.Debug().Interface("criteria", criteria).Msg("criteria") + s.receiveTransitions = append(s.receiveTransitions, ReceiveTransition{ + Transition: Transition{nextState: nextState}, + criteria: criteria, + }) + + return nextState +} + +// BeforeReceive Called with each tPDU received before matching. +func (s *state) BeforeReceive(pdu bacnetip.PDU) { + s.stateMachine.BeforeReceive(pdu) +} + +// AfterReceive Called with tPDU received after match. +func (s *state) AfterReceive(pdu bacnetip.PDU) { + s.stateMachine.AfterReceive(pdu) +} + +// Ignore Create a ReceiveTransition from this state to itself, if match is successful the effect is to Ignore the tPDU. +// +// criteria tPDU to match +func (s *state) Ignore(pduType any, pduAttrs map[string]any) State { + s.log.Debug().Interface("pduType", pduType).Interface("pduAttrs", pduAttrs).Msg("Ignore") + s.receiveTransitions = append(s.receiveTransitions, ReceiveTransition{ + Transition: Transition{}, + criteria: criteria{ + pduType: pduType, + pduAttrs: pduAttrs, + }, + }) + + return s +} + +// UnexpectedReceive Called with tPDU that did not match. Unless this is trapped by the state, the default behaviour is +// +// to fail. +func (s *state) UnexpectedReceive(pdu bacnetip.PDU) { + s.log.Debug().Stringer("pdu", pdu).Msg("UnexpectedReceive") + s.stateMachine.UnexpectedReceive(pdu) +} + +// SetEvent Create an EventTransition for this state that sets an event. The current state is returned for method +// +// chaining. event_id event identifier +func (s *state) SetEvent(eventId string) State { + s.log.Debug().Str("eventId", eventId).Msg("SetEvent") + s.setEventTransitions = append(s.setEventTransitions, EventTransition{ + Transition: Transition{}, + eventId: eventId, + }) + return s +} + +// EventSet Called with the event that was set. +func (s *state) EventSet(eventId string) { + // Nothing +} + +// ClearEvent Create an EventTransition for this state that clears an event. The current state is returned for method +// +// chaining. event_id event identifier +func (s *state) ClearEvent(eventId string) State { + s.log.Debug().Str("eventId", eventId).Msg("ClearEvent") + s.clearEventTransitions = append(s.clearEventTransitions, EventTransition{ + Transition: Transition{}, + eventId: eventId, + }) + return s +} + +// WaitEvent Create an EventTransition from this state to another, possibly new, state. The next state is returned for +// +// method chaining. pdu tPDU to send next_state state to transition to after sending +func (s *state) WaitEvent(eventId string, nextState State) State { + s.log.Debug().Str("eventId", eventId).Msg("WaitEvent") + if nextState == nil { + nextState = s.stateMachine.NewState("") + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) { + panic("off the rails") + } + + s.waitEventTransitions = append(s.waitEventTransitions, EventTransition{ + Transition: Transition{ + nextState: nextState, + }, + eventId: eventId, + }) + return nextState +} + +// Timeout Create a TimeoutTransition from this state to another, possibly new, +// +// state. There can only be one timeout transition per state. The next +// state is returned for method chaining. +// +// delay the amount of time to wait for a matching tPDU +// next_state destination state after timeout +func (s *state) Timeout(delay time.Duration, nextState State) State { + s.log.Debug().Dur("delay", delay).Stringer("nextState", nextState).Msg("Timeout") + if s.timeoutTransition != nil { + panic("state already has a timeout") + } + + if nextState == nil { + nextState = s.stateMachine.NewState("") + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) { + panic("off the rails") + } + + now := bacnetip.GetTaskManagerTime() + + s.timeoutTransition = &TimeoutTransition{ + Transition: Transition{nextState: nextState}, + timeout: now.Add(delay), + } + return nextState +} + +// Call Create a CallTransition from this state to another, possibly new, state. The next state is returned for method +// +// chaining. criteria tPDU to match next_state destination state after a successful match +func (s *state) Call(fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error, args bacnetip.Args, kwargs bacnetip.KWArgs) State { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Call") + if s.callTransition != nil { + panic("state already has a 'Call' per state") + } + var nextState State + if _nextState, ok := kwargs["next_state"]; ok { + nextState = _nextState.(State) + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + delete(kwargs, "next_state") + } + + if nextState == nil { + nextState = s.stateMachine.NewState("") + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) { + panic("off the rails") + } + + s.callTransition = &CallTransition{ + Transition: Transition{nextState: nextState}, + fnargs: fnargs{ + fn: fn, + args: args, + kwargs: kwargs, + }, + } + return nextState +} + +type StateMachineRequirements interface { + Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +// StateMachine A state machine consisting of states. Every state machine has a start +// +// state where the state machine begins when it is started. It also has +// an *unexpected receive* fail state where the state machine goes if +// there is an unexpected (unmatched) tPDU received. +type StateMachine interface { + fmt.Stringer + + NewState(string) State + UnexpectedReceive(pdu bacnetip.PDU) + BeforeSend(pdu bacnetip.PDU) + Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error + AfterSend(pdu bacnetip.PDU) + BeforeReceive(pdu bacnetip.PDU) + Receive(args bacnetip.Args, kwargs bacnetip.KWArgs) error + AfterReceive(pdu bacnetip.PDU) + EventSet(id string) + Run() error + Reset() + GetTransactionLog() []string + GetCurrentState() State + GetUnexpectedReceiveState() State + GetStartState() State + IsRunning() bool + IsSuccessState() bool + IsFailState() bool + + getStateTimeoutTask() *TimeoutTask + getStates() []State + getCurrentState() State + setMachineGroup(machineGroup *StateMachineGroup) + getMachineGroup() *StateMachineGroup + halt() +} + +type stateMachine struct { + StateMachineRequirements + + interceptor StateInterceptor + stateDecorator func(state State) State + + states []State + name string + machineGroup *StateMachineGroup + stateSubStruct any + startState State + unexpectedReceiveState State + transitionQueue chan bacnetip.PDU + stateTimeoutTask *TimeoutTask + timeout time.Duration + timeoutState State + stateMachineTimeout *time.Time + timeoutTask *TimeoutTask + running bool + startupFlag bool + isSuccessState *bool + isFailState *bool + stateTransitioning int + currentState State + transactionLog []string + + log zerolog.Logger +} + +// NewStateMachine creates a new state machine. Make sure to call the init function (Init must be called after a new (can't be done in constructor as initialization is then not yet finished)) +func NewStateMachine(localLog zerolog.Logger, stateMachineRequirements StateMachineRequirements, opts ...func(machine *stateMachine)) (sm StateMachine, init func()) { + s := &stateMachine{ + StateMachineRequirements: stateMachineRequirements, + + log: localLog, + } + for _, opt := range opts { + opt(s) + } + if s.name != "" { + s.log = s.log.With().Str("name", s.name).Logger() + } + if s.stateDecorator == nil { + s.stateDecorator = func(state State) State { + return state + } + } + return s, func() { + s.Reset() + + if s.startState != nil { + if s.startState.getStateMachine() != nil { + panic("start state already bound to a machine") + } + s.states = append(s.states, s.startState) + s.startState.setStateMachine(s) + } else { + s.startState = s.NewState("start") + s.startState = s.stateDecorator(s.startState) + } + + if s.unexpectedReceiveState != nil { + if s.unexpectedReceiveState.getStateMachine() != nil { + panic("start state already bound to a machine") + } + s.states = append(s.states, s.unexpectedReceiveState) + s.unexpectedReceiveState.setStateMachine(s) + } else { + s.unexpectedReceiveState = s.NewState("unexpected receive").Fail("") + s.unexpectedReceiveState = s.stateDecorator(s.unexpectedReceiveState) + } + + s.transitionQueue = make(chan bacnetip.PDU, 100) + + s.stateTimeoutTask = NewTimeoutTask(s.StateTimeout, bacnetip.NoArgs, bacnetip.NoKWArgs, nil) + + if s.timeout != 0 { + s.timeoutState = s.NewState("state machine timeout").Fail("") + s.timeoutTask = NewTimeoutTask(s.StateMachineTimeout, bacnetip.NoArgs, bacnetip.NoKWArgs, s.stateMachineTimeout) + } + } +} + +func WithStateMachineName(name string) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.name = name + } +} + +func WithStateMachineStateInterceptor(interceptor StateInterceptor) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.interceptor = interceptor + } +} + +func WithStateMachineTimeout(timeout time.Duration) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.timeout = timeout + } +} + +func WithStateMachineStartState(startState State) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.startState = startState + } +} + +func WithStateMachineUnexpectedReceiveState(unexpectedReceiveState State) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.unexpectedReceiveState = unexpectedReceiveState + } +} + +func WithStateMachineMachineGroup(machineGroup *StateMachineGroup) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.machineGroup = machineGroup + } +} + +func WithStateMachineStateDecorator(stateDecorator func(state State) State) func(stateMachine *stateMachine) { + return func(stateMachine *stateMachine) { + stateMachine.stateDecorator = stateDecorator + } +} + +func (s *stateMachine) getStateTimeoutTask() *TimeoutTask { + return s.stateTimeoutTask +} + +func (s *stateMachine) getStates() []State { + return s.states +} + +func (s *stateMachine) getMachineGroup() *StateMachineGroup { + return s.machineGroup +} + +func (s *stateMachine) getCurrentState() State { + return s.currentState +} + +func (s *stateMachine) setMachineGroup(machineGroup *StateMachineGroup) { + s.machineGroup = machineGroup +} + +func (s *stateMachine) GetTransactionLog() []string { + return s.transactionLog +} + +func (s *stateMachine) GetCurrentState() State { + return s.currentState +} + +func (s *stateMachine) GetStartState() State { + return s.startState +} + +func (s *stateMachine) String() string { + return fmt.Sprintf("stateMachine(name=%s)", s.name) +} + +func (s *stateMachine) NewState(docString string) State { + s.log.Trace().Str("docString", docString).Msg("NewState") + _state := NewState(s.log, s, docString, WithStateStateInterceptor(s.interceptor)) + _state = s.stateDecorator(_state) + s.states = append(s.states, _state) + return _state +} + +func (s *stateMachine) Reset() { + s.log.Trace().Msg("Reset") + // make sure we're not running + if s.running { + panic("state machine is running") + } + + // flags for remembering Success or fail + s.isSuccessState = nil + s.isFailState = nil + + // no current state, empty transaction log + s.currentState = nil + s.transactionLog = make([]string, 0) + + // we are not starting up + s.startupFlag = false + + // give all the getStates a chance to reset + for _, state := range s.states { + state.Reset() + } +} + +func (s *stateMachine) Run() error { + s.log.Trace().Msg("Run") + if s.running { + panic("state machine is running") + } + if s.currentState != nil { + panic("not running but has a current state") + } + + if s.timeoutTask != nil { + s.log.Debug().Msg("schedule runtime limit") + s.timeoutTask.InstallTask(bacnetip.InstallTaskOptions{Delta: &s.timeout}) + } + + // we are starting up + s.startupFlag = true + + // go to the start state + if err := s.gotoState(s.startState); err != nil { + return errors.Wrap(err, "error going to start state") + } + + // startup complete + s.startupFlag = false + + // if it is part of a group, let the group know + if s.machineGroup != nil { + s.machineGroup.Started(s) + + // if it is stopped already, let the group know + if !s.running { + s.machineGroup.Stopped(s) + } + } + return nil +} + +// Called when the state machine should no longer be running. +func (s *stateMachine) halt() { + s.log.Trace().Msg("Halt") + // make sure we're running + if !s.running { + panic("state machine is not running") + } + + // cancel the timeout task + if s.timeoutTask != nil { + s.log.Debug().Msg("cancel runtime limit") + s.timeoutTask.SuspendTask() + } + + close(s.transitionQueue) + + // no longer running + s.running = false +} + +// success Called when the state machine has successfully completed. +func (s *stateMachine) success() { + s.log.Trace().Msg("Success") + isSuccessState := true + s.isSuccessState = &isSuccessState +} + +// success Called when the state machine has successfully completed. +func (s *stateMachine) fail() { + s.log.Trace().Msg("Fail") + isFailState := true + s.isFailState = &isFailState +} + +func (s *stateMachine) gotoState(state State) error { + s.log.Debug().Stringer("state", state).Msg("gotoState") + //where do you think you're going? + if !slices.ContainsFunc(s.states, state.Equals) { + return errors.New("off the rails") + } + + s.stateTransitioning += 1 + + if s.currentState != nil { + s.currentState.ExitState() + } else if state == s.startState { + // starting up + s.running = true + } else { + return errors.New("start at the start state") + } + + s.currentState = state + currentState := state + + currentState.EnterState() + s.log.Trace().Msg("state entered") + + if s.machineGroup != nil { + for _, transition := range currentState.getSetEventTransitions() { + s.log.Debug().Str("eventId", transition.eventId).Msg("setting event") + s.machineGroup.SetEvent(transition.eventId) + } + + for _, transition := range currentState.getClearEventTransitions() { + s.log.Debug().Str("eventId", transition.eventId).Msg("clearing event") + s.machineGroup.ClearEvent(transition.eventId) + } + } + + if currentState.IsSuccessState() { + s.log.Trace().Msg("Success state") + s.stateTransitioning -= 1 + + s.halt() + s.success() + + if s.machineGroup != nil && s.startupFlag { + s.machineGroup.Stopped(s) + } + + return nil + } + + if currentState.IsFailState() { + s.log.Trace().Msg("Fail state") + s.stateTransitioning -= 1 + + s.halt() + s.fail() + + if s.machineGroup != nil && s.startupFlag { + s.machineGroup.Stopped(s) + } + + return nil + } + + var nextState State + + if s.machineGroup != nil { + didBreak := false + for _, transition := range currentState.getWaitEventTransitions() { + s.log.Debug().Str("eventID", transition.eventId).Msg("waiting event") + if _, ok := s.machineGroup.events[transition.eventId]; ok { + nextState = transition.nextState + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + if nextState != currentState { + didBreak = true + break + } + } + } + if !didBreak { + s.log.Trace().Msg("no events already set") + } + } else { + s.log.Trace().Msg("not part of a group") + } + + if callTransition := currentState.getCallTransition(); callTransition != nil { + s.log.Debug().Interface("callTransition", callTransition).Msg("calling transition") + f := callTransition.fnargs + fn, args, kwargs := f.fn, f.args, f.kwargs + if err := fn(args, kwargs); err != nil { + var assertionError AssertionError + if !errors.As(err, &assertionError) { + return err + } + s.log.Trace().Err(err).Msg("called exception") + s.stateTransitioning -= 1 + + s.halt() + s.fail() + + if s.machineGroup != nil && !s.startupFlag { + s.machineGroup.Stopped(s) + } + + return nil + } else { + s.log.Trace().Msg("called, exception") + } + + nextState = callTransition.nextState + } else { + s.log.Trace().Msg("no calls") + } + + if nextState == nil { + for _, transition := range currentState.getSendTransitions() { + s.log.Debug().Stringer("transition", transition).Msg("sending transition") + currentState.getInterceptor().BeforeSend(transition.pdu) + if err := s.StateMachineRequirements.Send(bacnetip.NewArgs(transition.pdu), bacnetip.NewKWArgs()); err != nil { + return errors.Wrap(err, "failed to send") + } + currentState.getInterceptor().AfterSend(transition.pdu) + + nextState = transition.nextState + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + + if nextState != currentState { + break + } + } + } + + if nextState == nil { + s.log.Trace().Msg("nowhere to go") + } else if nextState == s.currentState { + s.log.Trace().Msg("going nowhere") + } else { + s.log.Trace().Msg("going") + if err := s.gotoState(nextState); err != nil { + return errors.Wrap(err, "error in recursion") + } + } + + s.stateTransitioning -= 1 + + if s.stateTransitioning == 0 { + queueRead: + for s.running { + select { + case pdu := <-s.transitionQueue: + if err := s.Receive(bacnetip.NewArgs(pdu), bacnetip.NewKWArgs()); err != nil { + return errors.Wrap(err, "failed to receive") + } + default: + break queueRead + } + } + } + return nil +} + +func (s *stateMachine) BeforeSend(pdu bacnetip.PDU) { + s.transactionLog = append(s.transactionLog, fmt.Sprintf("<<<%v", pdu)) +} + +func (s *stateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + panic("not implemented") +} + +func (s *stateMachine) AfterSend(pdu bacnetip.PDU) { +} + +func (s *stateMachine) BeforeReceive(pdu bacnetip.PDU) { + s.transactionLog = append(s.transactionLog, fmt.Sprintf(">>>%v", pdu)) +} + +func (s *stateMachine) Receive(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Receive") + pdu := args.Get0PDU() + if s.currentState == nil || s.stateTransitioning != 0 { + s.log.Trace().Msg("queue for later") + s.transitionQueue <- pdu + return nil + } + + if !s.running { + s.log.Trace().Msg("already completed") + return nil + } + + currentState := s.currentState + s.log.Debug().Stringer("currentState", currentState).Msg("current_state") + + currentState.getInterceptor().BeforeReceive(pdu) + + var nextState State + matchFound := false + for _, transition := range currentState.getReceiveTransitions() { + if s.MatchPDU(pdu, transition.criteria) { + s.log.Trace().Msg("match found") + matchFound = true + + currentState.getInterceptor().AfterReceive(pdu) + + nextState = transition.nextState + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + + if nextState != currentState { + break + } + } else { + s.log.Trace().Msg("no matches") + } + } + + if !matchFound { + currentState.getInterceptor().UnexpectedReceive(pdu) + } else if nextState != currentState { + if err := s.gotoState(nextState); err != nil { + return errors.Wrap(err, "error going to state") + } + } + return nil +} + +func (s *stateMachine) AfterReceive(pdu bacnetip.PDU) { + s.log.Trace().Stringer("pdu", pdu).Msg("AfterReceive") +} + +func (s *stateMachine) UnexpectedReceive(pdu bacnetip.PDU) { + s.log.Trace().Stringer("pdu", pdu).Msg("UnexpectedReceive") + s.log.Trace().Stringer("currentState", s.currentState).Msg("currentState") + if err := s.gotoState(s.unexpectedReceiveState); err != nil { + s.log.Error().Err(err).Msg("error going to unexpected state") + } +} + +func (s *stateMachine) GetUnexpectedReceiveState() State { + return s.unexpectedReceiveState +} + +func (s *stateMachine) EventSet(eventId string) { + s.log.Debug().Str("eventId", eventId).Msg("EventSet") + if !s.running { + s.log.Trace().Msg("not running") + return + } + + if s.stateTransitioning == 1 { + s.log.Trace().Msg("transitioning") + return + } + if s.currentState == nil { + panic("current state is nil") + } + currentState := s.currentState + + var nextState State + matchFound := false + for _, transition := range currentState.getWaitEventTransitions() { + if transition.eventId == eventId { + s.log.Trace().Msg("match found") + matchFound = true + + currentState.EventSet(eventId) + + nextState = transition.nextState + s.log.Debug().Stringer("nextState", nextState).Msg("nextState") + + if nextState != currentState { + break + } + } + } + if len(currentState.getWaitEventTransitions()) == 0 { + s.log.Trace().Msg("going nowhere") + } + + if matchFound && nextState != currentState { + s.log.Trace().Msg("going") + if err := s.gotoState(nextState); err != nil { + s.log.Error().Err(err).Msg("failed to go to next state") + } + } +} + +func (s *stateMachine) StateTimeout(_ bacnetip.Args, _ bacnetip.KWArgs) error { + s.log.Trace().Msg("StateTimeout") + if !s.running { + return errors.New("state machine is not running") + } + if s.currentState.getTimeoutTransition() == nil { + return errors.New("state timeout, but timeout transition is nil") + } + if err := s.gotoState(s.currentState.getTimeoutTransition().nextState); err != nil { + return errors.Wrap(err, "failed to go to next state") + } + return nil +} + +func (s *stateMachine) StateMachineTimeout(_ bacnetip.Args, _ bacnetip.KWArgs) error { + s.log.Trace().Msg("StateMachineTimeout") + if !s.running { + return errors.New("state machine is not running") + } + if err := s.gotoState(s.timeoutState); err != nil { + return errors.Wrap(err, "failed to go to next state") + } + return nil +} + +func (s *stateMachine) MatchPDU(pdu bacnetip.PDU, criteria criteria) bool { + s.log.Debug().Stringer("pdu", pdu).Stringer("criteria", criteria).Msg("MatchPDU") + return MatchPdu(s.log, pdu, criteria.pduType, criteria.pduAttrs) +} + +func (s *stateMachine) IsRunning() bool { + return s.running +} + +func (s *stateMachine) IsSuccessState() bool { + if s.isSuccessState == nil { + return false + } + return *s.isSuccessState +} + +func (s *stateMachine) IsFailState() bool { + if s.isFailState == nil { + return false + } + return *s.isFailState +} + +// StateMachineGroup A state machine group is a collection of state machines that are all +// +// started and stopped together. There are methods available to derived +// classes that are called when all of the machines in the group have +// completed, either all successfully or at least one has failed. +// +// .. note:: When creating a group of state machines, add the ones that +// are expecting to receive one or more tPDU's first before the ones +// that send tPDU's. They will be started first, and be ready for the +// tPDU that might be sent. +type StateMachineGroup struct { + stateMachines []StateMachine + isSuccessState bool + isFailState bool + events map[string]struct{} + startupFlag bool + isRunning bool + + log zerolog.Logger +} + +func NewStateMachineGroup(localLog zerolog.Logger) *StateMachineGroup { + return &StateMachineGroup{ + events: map[string]struct{}{}, + log: localLog, + } +} + +// Append Add a state machine to the end of the list of state machines +func (s *StateMachineGroup) Append(machine StateMachine) { + s.log.Debug().Stringer("stateMachine", machine).Msg("Append") + if machine.getMachineGroup() != nil { + panic("state machine group already contains this machine") + } + + machine.setMachineGroup(s) + + s.stateMachines = append(s.stateMachines, machine) +} + +// Remove a state machine from the list of state machines. +func (s *StateMachineGroup) Remove(machine StateMachine) { + s.log.Debug().Stringer("stateMachine", machine).Msg("Remove") + if machine.getMachineGroup() != s { + panic("state machine is not a member of this group") + } + + machine.setMachineGroup(nil) + for i, stateMachine := range s.stateMachines { + if stateMachine == machine { + s.stateMachines = append(s.stateMachines[:i], s.stateMachines[i+1:]...) + break + } + } +} + +// Reset resets all the machines in the group. +func (s *StateMachineGroup) Reset() { + s.log.Trace().Msg("Reset") + for _, stateMachine := range s.stateMachines { + s.log.Debug().Stringer("stateMachine", stateMachine).Msg("Resetting") + stateMachine.Reset() + } + + s.isSuccessState = false + s.isFailState = false + + s.events = make(map[string]struct{}) +} + +// SetEvent save an event as 'set' and pass it to the state machines to see +// +// if they are in a state that is waiting for the event. +func (s *StateMachineGroup) SetEvent(id string) { + s.log.Trace().Str("eventId", id).Msg("SetEvent") + s.events[id] = struct{}{} + + for _, machine := range s.stateMachines { + s.log.Debug().Stringer("stateMachine", machine).Msg("Setting") + machine.EventSet(id) + } +} + +// ClearEvent Remove an event from the set of elements that are 'set'. +func (s *StateMachineGroup) ClearEvent(id string) { + s.log.Trace().Str("eventId", id).Msg("ClearEvent") + delete(s.events, id) +} + +// Run Runs all the machines in the group. +func (s *StateMachineGroup) Run() error { + s.log.Trace().Msg("Run") + s.startupFlag = true + s.isRunning = true + + for _, machine := range s.stateMachines { + s.log.Debug().Stringer("stateMachine", machine).Msg("starting") + if err := machine.Run(); err != nil { + return errors.Wrap(err, "failed to start machine") + } + } + + s.startupFlag = false + s.log.Trace().Msg("all started") + + allSuccess, someFailed := s.CheckForSuccess() + if allSuccess { + s.Success() + } else if someFailed { + s.Fail() + } else { + s.log.Trace().Msg("some still running") + } + return nil +} + +// Started Called by a state machine in the group when it has completed its +// +// transition into its starting state. +func (s *StateMachineGroup) Started(machine *stateMachine) { + s.log.Debug().Stringer("stateMachine", machine).Msg("started") +} + +// Stopped Called by a state machine after it has halted and its Success() +// +// or fail() method has been called. +func (s *StateMachineGroup) Stopped(machine *stateMachine) { + s.log.Debug().Stringer("stateMachine", machine).Msg("stopped") + if s.startupFlag { + s.log.Trace().Msg("still starting up") + return + } + + allSuccess, someFailed := s.CheckForSuccess() + if allSuccess { + s.Success() + } else if someFailed { + s.Fail() + } else { + s.log.Trace().Msg("some still running") + } +} + +// CheckForSuccess Called after all of the machines have started, and each time a +// +// machine has stopped, to see if the entire group should be considered +// a Success or fail. +func (s *StateMachineGroup) CheckForSuccess() (allSuccess bool, someFailed bool) { + s.log.Trace().Msg("CheckForSuccess") + allSuccess = true + someFailed = false + + for _, machine := range s.stateMachines { + if machine.IsRunning() { + s.log.Trace().Stringer("machine", machine).Msg("running") + allSuccess = false + someFailed = false + break + } + + if machine.getCurrentState() == nil { + s.log.Trace().Stringer("machine", machine).Msg("not started") + allSuccess = false + someFailed = false + break + } + + allSuccess = allSuccess && machine.getCurrentState().IsSuccessState() + someFailed = someFailed || machine.getCurrentState().IsFailState() + } + s.log.Debug().Bool("allSuccess", allSuccess).Msg("allSuccess") + s.log.Debug().Bool("someFailed", allSuccess).Msg("someFailed") + return +} + +// Halt halts all of the running machines in the group. +func (s *StateMachineGroup) Halt() { + s.log.Trace().Msg("Halt") + for _, machine := range s.stateMachines { + if machine.IsRunning() { + machine.halt() + } + } +} + +// Success Called when all of the machines in the group have halted and they +// +// are all in a 'Success' final state. +func (s *StateMachineGroup) Success() { + s.log.Trace().Msg("Success") + s.isRunning = false + s.isSuccessState = true +} + +// Fail Called when all of the machines in the group have halted and at +// +// at least one of them is in a 'fail' final state. +func (s *StateMachineGroup) Fail() { + s.log.Trace().Msg("Fail") + s.isRunning = false + s.isFailState = true +} + +func (s *StateMachineGroup) GetStateMachines() []StateMachine { + return s.stateMachines +} + +func (s *StateMachineGroup) IsRunning() bool { + return s.isRunning +} + +func (s *StateMachineGroup) IsSuccessState() bool { + return s.isSuccessState +} + +func (s *StateMachineGroup) IsFailState() bool { + return s.isFailState +} + +// ClientStateMachine An instance of this class sits at the top of a stack. tPDU's that the +// +// state machine sends are sent down the stack and tPDU's coming up the +// stack are fed as received tPDU's. +type ClientStateMachine struct { + *bacnetip.Client + StateMachine + + name string + + log zerolog.Logger +} + +func NewClientStateMachine(localLog zerolog.Logger, opts ...func(*ClientStateMachine)) (*ClientStateMachine, error) { + c := &ClientStateMachine{ + log: localLog, + } + for _, opt := range opts { + opt(c) + } + var err error + c.Client, err = bacnetip.NewClient(localLog, c) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + var init func() + c.StateMachine, init = NewStateMachine(localLog, c, WithStateMachineName(c.name)) + init() + return c, nil +} + +func WithClientStateMachineName(name string) func(*ClientStateMachine) { + return func(c *ClientStateMachine) { + c.name = name + } +} + +func (s *ClientStateMachine) String() string { + return fmt.Sprintf("ClientStateMachine{Client: %v, StateMachine: %v}", s.Client, s.StateMachine) +} + +func (s *ClientStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send") + return s.Request(args, kwargs) +} + +func (s *ClientStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation") + return s.Receive(args, kwargs) +} + +type ServerStateMachine struct { + *bacnetip.Server + StateMachine + + name string + + log zerolog.Logger +} + +func NewServerStateMachine(localLog zerolog.Logger, opts ...func(*ServerStateMachine)) (*ServerStateMachine, error) { + c := &ServerStateMachine{ + log: localLog, + } + for _, opt := range opts { + opt(c) + } + var err error + c.Server, err = bacnetip.NewServer(localLog, c) + if err != nil { + return nil, errors.Wrap(err, "error creating Server") + } + var init func() + c.StateMachine, init = NewStateMachine(localLog, c, WithStateMachineName(c.name)) + init() + return c, nil +} + +func WithServerStateMachineName(name string) func(*ServerStateMachine) { + return func(s *ServerStateMachine) { + s.name = name + } +} + +func (s *ServerStateMachine) String() string { + return fmt.Sprintf("ServerStateMachine(TBD...)") // TODO: fill some info here +} + +func (s *ServerStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send") + return s.Response(args, kwargs) +} + +func (s *ServerStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + return s.Receive(args, kwargs) +} + +type TrafficLog struct { + traffic []struct { + time.Time + bacnetip.Args + } +} + +// Call Capture the current time and the arguments. +func (t *TrafficLog) Call(args bacnetip.Args) { + t.traffic = append(t.traffic, struct { + time.Time + bacnetip.Args + }{Time: time.Now(), Args: args}) +} + +// Dump the traffic, pass the correct handler like SomeClass._debug +func (t *TrafficLog) Dump(handlerFn func(format string, args bacnetip.Args)) { + if t == nil { + return + } + for _, args := range t.traffic { + argFormat := " %6.3f:" + for _, arg := range args.Args[1:] { + _ = arg + argFormat += " %v" + } + handlerFn(argFormat, args.Args) + } +} diff --git a/plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go b/plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go new file mode 100644 index 00000000000..704af458ea0 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_apdu + +import ( + "context" + "testing" + + "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMaxApduLengthAcceptedEncode(t *testing.T) { + t.Skip("Plc4x doesn't normalise at model level") + apdu := model.NewAPDU(50) + assert.Equal(t, 0, 50, apdu.ApduLength) +} + +func TestMaxApduLengthAcceptedDecode(t *testing.T) { + t.Skip("Plc4x doesn't normalise at model level") + apdu := model.NewAPDU(0) + serialize, err := apdu.Serialize() + require.NoError(t, err) + apduParse, err := model.APDUParse(context.Background(), serialize, 0) + require.NoError(t, err) + // TODO: no way to access the length + _ = apduParse +} diff --git a/plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go b/plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go new file mode 100644 index 00000000000..78b3f753f82 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_base_types + +// TODO: diff --git a/plc4go/internal/bacnetip/tests/test_bvll/helpers.go b/plc4go/internal/bacnetip/tests/test_bvll/helpers.go new file mode 100644 index 00000000000..c793f5b3e77 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_bvll/helpers.go @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_bvll + +import ( + "fmt" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type _NetworkServiceElement struct { + *bacnetip.NetworkServiceElement +} + +func new_NetworkServiceElement(localLog zerolog.Logger) (*_NetworkServiceElement, error) { + i := &_NetworkServiceElement{} + + // This class turns off the deferred startup function call that broadcasts + // I-Am-Router-To-Network and Network-Number-Is messages. + var err error + i.NetworkServiceElement, err = bacnetip.NewNetworkServiceElement(localLog, nil, true) + if err != nil { + return nil, errors.Wrap(err, "error creating network service element") + } + return i, nil +} + +type FauxMultiplexer struct { + *bacnetip.Client + *bacnetip.Server + + address *bacnetip.Address + unicastTuple *bacnetip.AddressTuple[string, uint16] + broadcastTuple *bacnetip.AddressTuple[string, uint16] + + node *bacnetip.IPNode + + log zerolog.Logger +} + +func NewFauxMultiplexer(localLog zerolog.Logger, addr *bacnetip.Address, network *bacnetip.IPNetwork) (*FauxMultiplexer, error) { + f := &FauxMultiplexer{ + address: addr, + log: localLog, + } + var err error + f.Client, err = bacnetip.NewClient(localLog, f) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + f.Server, err = bacnetip.NewServer(localLog, f) + if err != nil { + return nil, errors.Wrap(err, "error creating server") + } + + // get the unicast and broadcast tuples + f.unicastTuple = addr.AddrTuple + f.broadcastTuple = addr.AddrBroadcastTuple + + // make an internal node and bind to it, this takes the place of + // both the direct port and broadcast port of the real UDPMultiplexer + f.node, err = bacnetip.NewIPNode(localLog, addr, network) + if err != nil { + return nil, errors.Wrap(err, "error creating ip node") + } + if err := bacnetip.Bind(localLog, f, f.node); err != nil { + return nil, errors.Wrap(err, "error binding") + } + return f, nil +} + +func (s *FauxMultiplexer) String() string { + return fmt.Sprintf("FauxMultiplexer(TBD...)") // TODO: fill some info here +} + +func (s *FauxMultiplexer) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("Args", args).Stringer("KWArgs", kwargs).Msg("Indication") + + pdu := args.Get0PDU() + + var dest *bacnetip.Address + // check for a broadcast message + if pdu.GetPDUDestination().AddrType == bacnetip.LOCAL_BROADCAST_ADDRESS { + var err error + dest, err = bacnetip.NewAddress(s.log, s.broadcastTuple) + if err != nil { + return errors.Wrap(err, "error creating address") + } + s.log.Debug().Stringer("dest", dest).Msg("Requesting local broadcast") + } else if pdu.GetPDUDestination().AddrType == bacnetip.LOCAL_STATION_ADDRESS { + var err error + dest, err = bacnetip.NewAddress(s.log, pdu.GetPDUDestination().AddrAddress) + if err != nil { + return errors.Wrap(err, "error creating address") + } + s.log.Debug().Stringer("dest", dest).Msg("Requesting local station") + } else { + return errors.New("unknown destination type") + } + + unicast, err := bacnetip.NewAddress(s.log, s.unicastTuple) + if err != nil { + return errors.Wrap(err, "error creating address") + } + return s.Request(bacnetip.NewArgs(bacnetip.NewPDUFromPDU(pdu, bacnetip.WithPDUSource(unicast), bacnetip.WithPDUDestination(dest))), bacnetip.NoKWArgs) +} + +func (s *FauxMultiplexer) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("Args", args).Stringer("KWArgs", kwargs).Msg("Indication") + pdu := args.Get0PDU() + + // the PDU source and destination are tuples, convert them to Address instances + src := pdu.GetPDUSource() + + broadcast, err := bacnetip.NewAddress(s.log, s.broadcastTuple) + if err != nil { + return errors.Wrap(err, "error creating address") + } + var dest *bacnetip.Address + // see if the destination was our broadcast address + if pdu.GetPDUDestination().Equals(broadcast) { + dest = bacnetip.NewLocalBroadcast(nil) + } else { + dest, err = bacnetip.NewAddress(s.log, pdu.GetPDUDestination().AddrAddress) + if err != nil { + return errors.Wrap(err, "error creating address") + } + } + + return s.Response(bacnetip.NewArgs(bacnetip.NewPDUFromPDU(pdu, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))), bacnetip.NoKWArgs) +} + +type SnifferStateMachine struct { + *tests.ClientStateMachine + + name string + address *bacnetip.Address + annexj *bacnetip.AnnexJCodec + mux *FauxMultiplexer + + log zerolog.Logger +} + +func NewSnifferStateMachine(localLog zerolog.Logger, address string, vlan *bacnetip.IPNetwork) (*SnifferStateMachine, error) { + s := &SnifferStateMachine{ + log: localLog, + } + machine, err := tests.NewClientStateMachine(localLog) + if err != nil { + return nil, errors.Wrap(err, "error building client state machine") + } + s.ClientStateMachine = machine + + // save the name and address + s.name = address + s.address, err = bacnetip.NewAddress(localLog, address) + if err != nil { + return nil, errors.Wrap(err, "error creating address") + } + + // BACnet/IP interpreter + s.annexj, err = bacnetip.NewAnnexJCodec(localLog) + if err != nil { + return nil, errors.Wrap(err, "error creating annexj") + } + + // fake multiplexer has a VLAN node in it + s.mux, err = NewFauxMultiplexer(localLog, s.address, vlan) + if err != nil { + return nil, errors.Wrap(err, "error creating faux multiplexer") + } + + // might receive all packets and allow spoofing + s.mux.node.SetPromiscuous(true) + s.mux.node.SetSpoofing(true) + + // bind the stack together + if err := bacnetip.Bind(localLog, s, s.annexj, s.mux); err != nil { + return nil, errors.Wrap(err, "error binding") + } + + return s, nil +} + +type BIPStateMachine struct { + *tests.ClientStateMachine +} + +// TODO: implement BIPStateMachine + +type BIPSimpleStateMachine struct { + *tests.ClientStateMachine + name string + + address *bacnetip.Address + + bip *bacnetip.BIPSimple + annexj *bacnetip.AnnexJCodec + mux *FauxMultiplexer + + log zerolog.Logger +} + +func NewBIPSimpleStateMachine(name string, localLog zerolog.Logger, netstring string, vlan *bacnetip.IPNetwork) (*BIPSimpleStateMachine, error) { + address, err := bacnetip.NewAddress(localLog, netstring) + if err != nil { + return nil, errors.Wrap(err, "error building address") + } + stateMachine := &BIPSimpleStateMachine{ + // save the name and address + name: netstring, + address: address, + log: localLog, + } + clientStateMachine, err := tests.NewClientStateMachine(localLog, tests.WithClientStateMachineName(name)) + if err != nil { + return nil, errors.Wrap(err, "error building client state machine") + } + stateMachine.ClientStateMachine = clientStateMachine + + // BACnet/IP interpreter + stateMachine.bip, err = bacnetip.NewBIPSimple(localLog) + if err != nil { + return nil, errors.Wrap(err, "error building bip simple") + } + stateMachine.annexj, err = bacnetip.NewAnnexJCodec(localLog) + if err != nil { + return nil, errors.Wrap(err, "error building annexj codec") + } + + // fake multiplexer has a VLAN node in it + stateMachine.mux, err = NewFauxMultiplexer(localLog, stateMachine.address, vlan) + if err != nil { + return nil, errors.Wrap(err, "error creating faux") + } + + // bind the stack together + if err := bacnetip.Bind(localLog, stateMachine, stateMachine.bip, stateMachine.annexj, stateMachine.mux); err != nil { + return nil, errors.Wrap(err, "error binding") + } + + return stateMachine, nil +} + +type BIPForeignStateMachine struct { + *tests.ClientStateMachine +} + +type BIPBBMDStateMachine struct { + *tests.ClientStateMachine +} + +type BIPSimpleNode struct { +} + +type BIPBBMDNode struct { +} + +type TestDeviceObject struct { + *bacnetip.LocalDeviceObject +} + +type BIPSimpleApplicationLayerStateMachine struct { + *bacnetip.ApplicationServiceElement + *tests.ClientStateMachine +} + +type BIPBBMDApplication struct { + *bacnetip.Application + *bacnetip.WhoIsIAmServices + *bacnetip.ReadWritePropertyServices +} diff --git a/plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go b/plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go new file mode 100644 index 00000000000..6b237ffae0c --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_bvll + +import ( + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TNetwork struct { + *tests.StateMachineGroup + vlan *bacnetip.IPNetwork + td *BIPSimpleStateMachine + iut *BIPSimpleStateMachine + sniffer *SnifferStateMachine + + t *testing.T + + log zerolog.Logger +} + +func NewTNetwork(t *testing.T) *TNetwork { + localLog := testutils.ProduceTestingLogger(t) + tn := &TNetwork{ + t: t, + log: localLog, + } + tn.StateMachineGroup = tests.NewStateMachineGroup(localLog) + + tests.NewGlobalTimeMachine(localLog) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + localLog.Trace().Msg("time machine reset") + + // make a little LAN + tn.vlan = bacnetip.NewIPNetwork(localLog) + + // Test devices + var err error + tn.td, err = NewBIPSimpleStateMachine("td", localLog, "192.168.4.1/24", tn.vlan) + require.NoError(t, err) + tn.Append(tn.td) + + // implementation under test + tn.iut, err = NewBIPSimpleStateMachine("iut", localLog, "192.168.4.2/24", tn.vlan) + require.NoError(t, err) + tn.Append(tn.iut) + + // sniffer node + tn.sniffer, err = NewSnifferStateMachine(localLog, "192.168.4.254/24", tn.vlan) + require.NoError(t, err) + tn.Append(tn.sniffer) + return tn +} + +func (t *TNetwork) Run(timeLimit time.Duration) { + if timeLimit == 0 { + timeLimit = 60 * time.Second + } + t.log.Debug().Dur("time_limit", timeLimit).Msg("run") + + // run the group + err := t.StateMachineGroup.Run() + require.NoError(t.t, err) + + // run it some time + tests.RunTimeMachine(t.log, timeLimit, time.Time{}) + t.log.Trace().Msg("time machine finished") + for _, machine := range t.StateMachineGroup.GetStateMachines() { + t.log.Debug().Stringer("machine", machine).Msg("Machine:") + for _, s := range machine.GetTransactionLog() { + t.log.Debug().Str("logEntry", s).Msg("logEntry") + } + } + + // check for success + success, failed := t.CheckForSuccess() + assert.True(t.t, success) + assert.False(t.t, failed) +} + +func TestSimple(t *testing.T) { + t.Run("test_idle", func(t *testing.T) { //Test an idle network, nothing happens is success. + tests.LockGlobalTimeMachine(t) + tnet := NewTNetwork(t) + + // all start state are successful + tnet.td.GetStartState().Success("") + tnet.iut.GetStartState().Success("") + tnet.sniffer.GetStartState().Success("") + + // run the group + tnet.Run(0) + }) + t.Run("test_unicast", func(t *testing.T) { //Test a unicast message from TD to IUT. + t.Skip("not ready yet") // TODO: figure out why it is failing + tests.LockGlobalTimeMachine(t) + tnet := NewTNetwork(t) + + //make a PDU from node 1 to node 2 + pduData, err := bacnetip.Xtob("dead.beef") + require.NoError(t, err) + apdu := model.NewAPDUUnknown(0, pduData, 0) + control := model.NewNPDUControl(false, true, true, false, model.NPDUNetworkPriority_CRITICAL_EQUIPMENT_MESSAGE) + sourceAddr := tnet.td.address + destAddr := tnet.iut.address + npdu := model.NewNPDU(0, + control, + destAddr.AddrNet, + destAddr.AddrLen, + destAddr.AddrAddress, + sourceAddr.AddrNet, + sourceAddr.AddrLen, + sourceAddr.AddrAddress, + nil, + nil, + apdu, + 0) + t.Logf("pdu: \n%v", npdu) + + // test device sends it, iut gets it + pdu := bacnetip.NewPDU(npdu) + pdu.SetPDUSource(tnet.td.address) + pdu.SetPDUDestination(tnet.iut.address) + tnet.td.GetStartState().Send(pdu, nil).Success("") + tnet.iut.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": tnet.td.address, + }).Success("") + + // sniffer sees message on the wire + tnet.sniffer.GetStartState().Receive(bacnetip.NewPDU(npdu), map[string]any{ + "pduSource": tnet.td.address.AddrTuple, + "pduDestination": tnet.iut.address.AddrTuple, + "pduData": pduData, + }, + ).Timeout(1.0*time.Millisecond, nil).Success("") + + // run the group + tnet.Run(0) + }) + t.Run("test_broadcast", func(t *testing.T) { //Test a broadcast message from TD to IUT. + t.Skip("not ready yet") // TODO: figure out why it is failing + tests.LockGlobalTimeMachine(t) + tnet := NewTNetwork(t) + + //make a PDU from node 1 to node 2 + pduData, err := bacnetip.Xtob("dead.beef") + require.NoError(t, err) + apdu := model.NewAPDUUnknown(0, pduData, 0) + control := model.NewNPDUControl(false, true, true, false, model.NPDUNetworkPriority_CRITICAL_EQUIPMENT_MESSAGE) + sourceAddr := tnet.td.address + destAddr := bacnetip.NewLocalBroadcast(nil) + { + // TODO: why is this uncommented upstream + destAddr, err = bacnetip.NewAddress(testutils.ProduceTestingLogger(t), "192.168.4.255") + require.NoError(t, err) + } + npdu := model.NewNPDU(0, + control, + destAddr.AddrNet, + destAddr.AddrLen, + destAddr.AddrAddress, + sourceAddr.AddrNet, + sourceAddr.AddrLen, + sourceAddr.AddrAddress, + nil, + nil, + apdu, + 0) + t.Logf("pdu: \n%v", npdu) + + // test device sends it, iut gets it + tnet.td.GetStartState().Send(bacnetip.NewPDU(npdu, bacnetip.WithPDUSource(tnet.td.address), bacnetip.WithPDUDestination(bacnetip.NewLocalBroadcast(nil))), nil).Success("") + tnet.iut.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": tnet.td.address, + }).Success("") + + // sniffer sees message on the wire + tnet.sniffer.GetStartState().Receive(bacnetip.NewPDU(npdu), map[string]any{ + "pduSource": tnet.td.address.AddrTuple, + //"pduDestination": tnet.iut.address.AddrTuple, + "pduData": pduData, + }, + ).Timeout(1.0*time.Second, nil).Success("") + + // run the group + tnet.Run(0) + }) +} diff --git a/plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go b/plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go new file mode 100644 index 00000000000..f112bd5fe4c --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go @@ -0,0 +1,649 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_pdu + +import ( + "encoding/hex" + "testing" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Assert that the type, network, length, and address are what +// +// they should be. Note that the address parameter is a hex string +// that will be converted to bytes for comparison. +// +// :param addr: the address to match +// :param t: the address type +// :param n: the network number +// :param l: the address length +// :param a: the address expressed as hex bytes +func matchAddress(_t *testing.T, addr *bacnetip.Address, t bacnetip.AddressType, n *uint16, l *uint8, a string) { + _t.Helper() + assert.Equal(_t, addr.AddrType, t) + assert.Equal(_t, addr.AddrNet, n) + assert.Equal(_t, addr.AddrLen, l) + if a == "" { + assert.Nil(_t, addr.AddrAddress) + } else { + decodeString, err := hex.DecodeString(a) + require.NoError(_t, err) + assert.Equal(_t, addr.AddrAddress, decodeString) + } +} + +func init() { // TODO: maybe put in a setupsuite + bacnetip.Settings.RouteAware = true +} + +func TestAddress(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // null address + testAddr, err := bacnetip.NewAddress(testingLogger) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.NULL_ADDRESS, nil, nil, "") +} + +func TestAddressInt(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test integer local station + testAddr, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01") + assert.Equal(t, "1", testAddr.String()) + + testAddr, err = bacnetip.NewAddress(testingLogger, 254) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "fe") + assert.Equal(t, "254", testAddr.String()) + + // Test bad integer + _, err = bacnetip.NewAddress(testingLogger, -1) + assert.Error(t, err) + + _, err = bacnetip.NewAddress(testingLogger, 256) + assert.Error(t, err) +} + +func TestAddressIpv4Str(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test IPv4 local station address + testAddr, err := bacnetip.NewAddress(testingLogger, "1.2.3.4") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304BAC0") + assert.Equal(t, "1.2.3.4", testAddr.String()) + + // test IPv4 local station address with non-standard port + testAddr, err = bacnetip.NewAddress(testingLogger, "1.2.3.4:47809") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304BAC1") + assert.Equal(t, "1.2.3.4:47809", testAddr.String()) + + // test IPv4 local station address with unrecognized port + testAddr, err = bacnetip.NewAddress(testingLogger, "1.2.3.4:47999") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304bb7f") + assert.Equal(t, "0x01020304bb7f", testAddr.String()) +} + +func TestAddressEthStr(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test IPv4 local station address + testAddr, err := bacnetip.NewAddress(testingLogger, "01:02:03:04:05:06") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "010203040506") + assert.Equal(t, "0x010203040506", testAddr.String()) +} + +func TestAddressLocalStationStr(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test integer local station + testAddr, err := bacnetip.NewAddress(testingLogger, "1") + require.NoError(t, err) + assert.Equal(t, "1", testAddr.String()) + + testAddr, err = bacnetip.NewAddress(testingLogger, "254") + require.NoError(t, err) + assert.Equal(t, "254", testAddr.String()) + + // Test bad integer + _, err = bacnetip.NewAddress(testingLogger, 256) + assert.Error(t, err) + + // test modern hex string + testAddr, err = bacnetip.NewAddress(testingLogger, "0x01") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01") + assert.Equal(t, "1", testAddr.String()) + + testAddr, err = bacnetip.NewAddress(testingLogger, "0x0102") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(2), "0102") + assert.Equal(t, "0x0102", testAddr.String()) + + // test old school hex string + testAddr, err = bacnetip.NewAddress(testingLogger, "X'01'") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01") + assert.Equal(t, "1", testAddr.String()) + + testAddr, err = bacnetip.NewAddress(testingLogger, "X'0102'") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(2), "0102") + assert.Equal(t, "0x0102", testAddr.String()) +} + +func TestAddressLocalBroadcastStr(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test IPv4 local station address + testAddr, err := bacnetip.NewAddress(testingLogger, "*") + require.NoError(t, err) + assert.Equal(t, "*", testAddr.String()) +} + +func TestAddressRemoteBroadcastStr(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test IPv4 local station address + testAddr, err := bacnetip.NewAddress(testingLogger, "1:*") + require.NoError(t, err) + assert.Equal(t, "1:*", testAddr.String()) +} + +func TestAddressRemoteStationStr(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test IPv4 local station address + testAddr, err := bacnetip.NewAddress(testingLogger, "1:2") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "02") + assert.Equal(t, "1:2", testAddr.String()) + + testAddr, err = bacnetip.NewAddress(testingLogger, "1:254") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe") + assert.Equal(t, "1:254", testAddr.String()) + + // test bad network and mode + _, err = bacnetip.NewAddress(testingLogger, "65536:2") + assert.Error(t, err) + _, err = bacnetip.NewAddress(testingLogger, "1:256") + assert.Error(t, err) + + // test moder hex string + testAddr, err = bacnetip.NewAddress(testingLogger, "1:0x02") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "02") + assert.Equal(t, "1:2", testAddr.String()) + + // test bad network + _, err = bacnetip.NewAddress(testingLogger, "65536:0x02") + assert.Error(t, err) + + testAddr, err = bacnetip.NewAddress(testingLogger, "1:0x0203") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0203") + assert.Equal(t, "1:0x0203", testAddr.String()) + + // test old school hex + testAddr, err = bacnetip.NewAddress(testingLogger, "1:X'02'") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "02") + assert.Equal(t, "1:2", testAddr.String()) + + testAddr, err = bacnetip.NewAddress(testingLogger, "1:X'0203'") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0203") + assert.Equal(t, "1:0x0203", testAddr.String()) + + _, err = bacnetip.NewAddress(testingLogger, "65536:X'02'") + assert.Error(t, err) +} + +func TestAddressGlobalBroadcastStr(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // test IPv4 local station address + testAddr, err := bacnetip.NewAddress(testingLogger, "*:*") + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "") + assert.Equal(t, "*:*", testAddr.String()) +} + +func TestLocalStation(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // one Parameter + _, err := bacnetip.NewLocalStation(testingLogger, nil, nil) + require.Error(t, err) + + // test integer + testAddr, err := bacnetip.NewLocalStation(testingLogger, 1, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01") + assert.Equal(t, "1", testAddr.String()) + + testAddr, err = bacnetip.NewLocalStation(testingLogger, 254, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "fe") + assert.Equal(t, "254", testAddr.String()) + + // Test bad integer + _, err = bacnetip.NewLocalStation(testingLogger, -1, nil) + require.Error(t, err) + _, err = bacnetip.NewLocalStation(testingLogger, 256, nil) + require.Error(t, err) + + // Test bytes + xtob, err := bacnetip.Xtob("01") + require.NoError(t, err) + testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01") + assert.Equal(t, "1", testAddr.String()) + xtob, err = bacnetip.Xtob("fe") + require.NoError(t, err) + testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "fe") + assert.Equal(t, "254", testAddr.String()) + + // multi-byte strings are hex encoded + xtob, err = bacnetip.Xtob("0102") + require.NoError(t, err) + testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(2), "0102") + assert.Equal(t, "0x0102", testAddr.String()) + + xtob, err = bacnetip.Xtob("010203") + require.NoError(t, err) + testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(3), "010203") + assert.Equal(t, "0x010203", testAddr.String()) + + // match and IP address + xtob, err = bacnetip.Xtob("01020304bac0") + require.NoError(t, err) + testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304bac0") + assert.Equal(t, "1.2.3.4", testAddr.String()) +} + +func TestRemoteStation(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // two Parameters, correct types + _, err := bacnetip.NewRemoteStation(testingLogger, nil, nil, nil) + require.Error(t, err) + + // test bad network + _, err = bacnetip.NewRemoteStation(testingLogger, nil, -11, nil) + require.Error(t, err) +} + +func TestRemoteStationInts(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + net := func(i uint16) *uint16 { + return &i + } + + // testInteger + testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), 1, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "01") + assert.Equal(t, "1:1", testAddr.String()) + + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe") + assert.Equal(t, "1:254", testAddr.String()) + + // test station address + _, err = bacnetip.NewRemoteStation(testingLogger, nil, -1, nil) + require.Error(t, err) + _, err = bacnetip.NewRemoteStation(testingLogger, nil, 256, nil) + require.Error(t, err) +} + +func TestRemoteStationBytes(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + net := func(i uint16) *uint16 { + return &i + } + + // multi-byte strings are hex encoded + xtob, err := bacnetip.Xtob("0102") + require.NoError(t, err) + testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0102") + assert.Equal(t, "1:0x0102", testAddr.String()) + + xtob, err = bacnetip.Xtob("010203") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203") + assert.Equal(t, "1:0x010203", testAddr.String()) + + // match with IPv4 address + xtob, err = bacnetip.Xtob("01020304bac0") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0") + assert.Equal(t, "1:1.2.3.4", testAddr.String()) + + xtob, err = bacnetip.Xtob("01020304bac1") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac1") + assert.Equal(t, "1:1.2.3.4:47809", testAddr.String()) +} + +func TestRemoteStationIntsRouted(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + net := func(i uint16) *uint16 { + return &i + } + + Address := func(a string) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + + // testInteger + testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), 1, Address("1.2.3.4")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "01") + assert.Equal(t, "1:1@1.2.3.4", testAddr.String()) + + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("1.2.3.4")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe") + assert.Equal(t, "1:254@1.2.3.4", testAddr.String()) + + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("1.2.3.4:47809")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe") + assert.Equal(t, "1:254@1.2.3.4:47809", testAddr.String()) + + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("0x01020304BAC0")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe") + assert.Equal(t, "1:254@1.2.3.4", testAddr.String()) + + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("0x01020304BAC1")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe") + assert.Equal(t, "1:254@1.2.3.4:47809", testAddr.String()) + + // test station address + _, err = bacnetip.NewRemoteStation(testingLogger, nil, -1, nil) + require.Error(t, err) + _, err = bacnetip.NewRemoteStation(testingLogger, nil, 256, nil) + require.Error(t, err) +} + +func TestRemoteStationBytesRouted(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + net := func(i uint16) *uint16 { + return &i + } + + Address := func(a string) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + + // multi-byte strings are hex encoded + xtob, err := bacnetip.Xtob("0102") + require.NoError(t, err) + testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0102") + assert.Equal(t, "1:0x0102@1.2.3.4", testAddr.String()) + + xtob, err = bacnetip.Xtob("010203") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203") + assert.Equal(t, "1:0x010203@1.2.3.4", testAddr.String()) + + xtob, err = bacnetip.Xtob("010203") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4:47809")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203") + assert.Equal(t, "1:0x010203@1.2.3.4:47809", testAddr.String()) + + xtob, err = bacnetip.Xtob("010203") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC0")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203") + assert.Equal(t, "1:0x010203@1.2.3.4", testAddr.String()) + + xtob, err = bacnetip.Xtob("010203") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC1")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203") + assert.Equal(t, "1:0x010203@1.2.3.4:47809", testAddr.String()) + + // match with an IPv4 address + xtob, err = bacnetip.Xtob("01020304bac0") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0") + assert.Equal(t, "1:1.2.3.4@1.2.3.4", testAddr.String()) + + xtob, err = bacnetip.Xtob("01020304bac0") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4:47809")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0") + assert.Equal(t, "1:1.2.3.4@1.2.3.4:47809", testAddr.String()) + + xtob, err = bacnetip.Xtob("01020304bac0") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC0")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0") + assert.Equal(t, "1:1.2.3.4@1.2.3.4", testAddr.String()) + + xtob, err = bacnetip.Xtob("01020304bac0") + require.NoError(t, err) + testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC1")) + require.NoError(t, err) + matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0") + assert.Equal(t, "1:1.2.3.4@1.2.3.4:47809", testAddr.String()) +} + +func TestLocalBroadcast(t *testing.T) { + testAddr := bacnetip.NewLocalBroadcast(nil) + matchAddress(t, testAddr, bacnetip.LOCAL_BROADCAST_ADDRESS, nil, nil, "") + assert.Equal(t, "*", testAddr.String()) +} + +func TestLocalBroadcastRouted(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + Address := func(a string) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + + testAddr := bacnetip.NewLocalBroadcast(Address("1.2.3.4")) + matchAddress(t, testAddr, bacnetip.LOCAL_BROADCAST_ADDRESS, nil, nil, "") + assert.Equal(t, "*@1.2.3.4", testAddr.String()) +} + +func TestRemoteBroadcast(t *testing.T) { + testAddr := bacnetip.NewRemoteBroadcast(1, nil) + matchAddress(t, testAddr, bacnetip.REMOTE_BROADCAST_ADDRESS, n(1), nil, "") + assert.Equal(t, "1:*", testAddr.String()) +} + +func TestRemoteBroadcastRouted(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + Address := func(a string) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + + testAddr := bacnetip.NewRemoteBroadcast(1, Address("1.2.3.4")) + matchAddress(t, testAddr, bacnetip.REMOTE_BROADCAST_ADDRESS, n(1), nil, "") + assert.Equal(t, "1:*@1.2.3.4", testAddr.String()) +} + +func TestGlobalBroadcast(t *testing.T) { + testAddr := bacnetip.NewGlobalBroadcast(nil) + matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "") + assert.Equal(t, "*:*", testAddr.String()) +} + +func TestGlobalBroadcastRouted(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + Address := func(a string) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + + testAddr := bacnetip.NewGlobalBroadcast(Address("1.2.3.4")) + matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "") + assert.Equal(t, "*:*@1.2.3.4", testAddr.String()) + + testAddr = bacnetip.NewGlobalBroadcast(Address("1.2.3.4:47809")) + matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "") + assert.Equal(t, "*:*@1.2.3.4:47809", testAddr.String()) +} + +func TestAddressEquality(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + Address := func(a any) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + LocalStation := func(addr any) *bacnetip.Address { + station, err := bacnetip.NewLocalStation(testingLogger, addr, nil) + require.NoError(t, err) + return station + } + RemoteStation := func(net uint16, addr any) *bacnetip.Address { + station, err := bacnetip.NewRemoteStation(testingLogger, &net, addr, nil) + require.NoError(t, err) + return station + } + LocalBroadcast := func() *bacnetip.Address { + broadcast := bacnetip.NewLocalBroadcast(nil) + return broadcast + } + RemoteBroadcast := func(net uint16) *bacnetip.Address { + broadcast := bacnetip.NewRemoteBroadcast(net, nil) + return broadcast + } + GlobalBroadcast := func() *bacnetip.Address { + broadcast := bacnetip.NewGlobalBroadcast(nil) + return broadcast + } + + assert.True(t, Address(1).Equals(LocalStation(1))) + assert.True(t, Address("2").Equals(LocalStation(2))) + assert.True(t, Address("*").Equals(LocalBroadcast())) + assert.True(t, Address("3:4").Equals(RemoteStation(3, 4))) + assert.True(t, Address("5:*").Equals(RemoteBroadcast(5))) + assert.True(t, Address("*:*").Equals(GlobalBroadcast())) + +} + +func TestAddressEqualityRouted(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + Address := func(a any) *bacnetip.Address { + address, err := bacnetip.NewAddress(testingLogger, a) + require.NoError(t, err) + return address + } + RemoteStation := func(net uint16, addr any, route *bacnetip.Address) *bacnetip.Address { + station, err := bacnetip.NewRemoteStation(testingLogger, &net, addr, route) + require.NoError(t, err) + return station + } + RemoteBroadcast := func(net uint16, route *bacnetip.Address) *bacnetip.Address { + broadcast := bacnetip.NewRemoteBroadcast(net, route) + return broadcast + } + GlobalBroadcast := func(route *bacnetip.Address) *bacnetip.Address { + broadcast := bacnetip.NewGlobalBroadcast(route) + return broadcast + } + + assert.True(t, Address("3:4@6.7.8.9").Equals(RemoteStation(3, 4, Address("6.7.8.9")))) + assert.True(t, Address("3:4@0x06070809BAC0").Equals(RemoteStation(3, 4, Address("6.7.8.9")))) + + assert.True(t, Address("3:4@6.7.8.9:47809").Equals(RemoteStation(3, 4, Address("6.7.8.9:47809")))) + assert.True(t, Address("3:4@0x06070809BAC1").Equals(RemoteStation(3, 4, Address("6.7.8.9:47809")))) + + assert.True(t, Address("5:*@6.7.8.9").Equals(RemoteBroadcast(5, Address("6.7.8.9")))) + assert.True(t, Address("5:*@0x06070809BAC0").Equals(RemoteBroadcast(5, Address("6.7.8.9")))) + + assert.True(t, Address("5:*@6.7.8.9:47809").Equals(RemoteBroadcast(5, Address("6.7.8.9:47809")))) + assert.True(t, Address("5:*@0x06070809BAC1").Equals(RemoteBroadcast(5, Address("6.7.8.9:47809")))) + + assert.True(t, Address("*:*@6.7.8.9").Equals(GlobalBroadcast(Address("6.7.8.9")))) + assert.True(t, Address("*:*@0x06070809BAC0").Equals(GlobalBroadcast(Address("6.7.8.9")))) + + assert.True(t, Address("*:*@6.7.8.9:47809").Equals(GlobalBroadcast(Address("6.7.8.9:47809")))) + assert.True(t, Address("*:*@0x06070809BAC1").Equals(GlobalBroadcast(Address("6.7.8.9:47809")))) +} + +func n(n uint16) *uint16 { + return &n +} + +func l(l uint8) *uint8 { + return &l +} diff --git a/plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go b/plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go new file mode 100644 index 00000000000..cb4ce6131c2 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_pdu + +// TODO diff --git a/plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go b/plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go new file mode 100644 index 00000000000..def57c62a02 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_pdu + +// TODO: diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/todo b/plc4go/internal/bacnetip/tests/test_primitive_data/todo new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go b/plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go new file mode 100644 index 00000000000..b333fcc609b --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go @@ -0,0 +1,558 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_segmentation + +import ( + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" + "github.com/apache/plc4x/plc4go/spi/testutils" + "github.com/apache/plc4x/plc4go/spi/utils" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// This struct turns off the deferred startup function call that broadcasts I-Am-Router-To-Network and Network-Number-Is +// +// messages. +type _NetworkServiceElement struct { + *bacnetip.NetworkServiceElement +} + +func new_NetworkServiceElement(localLog zerolog.Logger) (*_NetworkServiceElement, error) { + n := &_NetworkServiceElement{} + var err error + n.NetworkServiceElement, err = bacnetip.NewNetworkServiceElement(localLog, nil, true) + if err != nil { + return nil, errors.Wrap(err, "error creating network service element") + } + return n, nil +} + +type ApplicationNetworkRequirements interface { + Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error + Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +type ApplicationNetwork struct { + ApplicationNetworkRequirements + *tests.StateMachineGroup + + trafficLog *tests.TrafficLog + vlan *bacnetip.Network + sniffer *SnifferNode + td *ApplicationStateMachine + iut *ApplicationStateMachine + + log zerolog.Logger +} + +func NewApplicationNetwork(localLog zerolog.Logger, applicationNetworkRequirements ApplicationNetworkRequirements, tdDeviceObject, iutDeviceObject *bacnetip.LocalDeviceObject) (*ApplicationNetwork, error) { + a := &ApplicationNetwork{ + ApplicationNetworkRequirements: applicationNetworkRequirements, + log: localLog, + } + a.StateMachineGroup = tests.NewStateMachineGroup(localLog) + + // Reset the time machine + tests.ResetTimeMachine(time.Time{}) + + // Create a traffic log + a.trafficLog = new(tests.TrafficLog) + + // make a little LAN + a.vlan = bacnetip.NewNetwork(a.log, bacnetip.WithNetworkBroadcastAddress(bacnetip.NewLocalBroadcast(nil)), bacnetip.WithNetworkTrafficLogger(a.trafficLog)) + + // sniffer + var err error + a.sniffer, err = NewSnifferNode(a.log, a.vlan) + if err != nil { + return nil, errors.Wrap(err, "error creating sniffer node") + } + + // test device + a.td, err = NewApplicationStateMachine(a.log, tdDeviceObject, a.vlan, a) + if err != nil { + return nil, errors.Wrap(err, "error creating application state machine") + } + a.Append(a.td) + + // implementation under test + a.iut, err = NewApplicationStateMachine(a.log, iutDeviceObject, a.vlan, a) + if err != nil { + return nil, errors.Wrap(err, "error creating application state machine") + } + a.Append(a.iut) + + return a, nil +} + +func (a *ApplicationNetwork) Run(timeLimit time.Duration) error { + if timeLimit == 0 { + timeLimit = 60 * time.Second + } + a.log.Debug().Dur("timeLimit", timeLimit).Msg("run") + + // Run the group + err := a.StateMachineGroup.Run() + if err != nil { + return errors.Wrap(err, "error running state machine group") + } + a.log.Trace().Msg("group running") + + // run it for some time + tests.RunTimeMachine(a.log, timeLimit, time.Time{}) + if a.log.Debug().Enabled() { + a.log.Debug().Msg("time machine finished") + for _, machine := range a.GetStateMachines() { + a.log.Debug().Stringer("machine", machine).Msg("machine") + for _, entry := range machine.GetTransactionLog() { + a.log.Debug().Str("entry", entry).Msg("transaction log entry") + } + } + + a.trafficLog.Dump(a._debug) + } + + // check for success + success, failed := a.CheckForSuccess() + if !success { + return errors.New("application network did not succeed") + } + _ = failed + + return nil +} + +func (a *ApplicationNetwork) _debug(format string, args bacnetip.Args) { + a.log.Debug().Msgf(format, args) +} + +type SnifferNode struct { + *bacnetip.Client + + name string + address *bacnetip.Address + node *bacnetip.Node + + log zerolog.Logger +} + +func NewSnifferNode(localLog zerolog.Logger, vlan *bacnetip.Network) (*SnifferNode, error) { + s := &SnifferNode{ + name: "sniffer", + log: localLog, + } + s.address, _ = bacnetip.NewAddress(localLog) + var err error + s.Client, err = bacnetip.NewClient(localLog, s) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + + // create a promiscuous node, added to the network + s.node, err = bacnetip.NewNode(localLog, s.address, vlan, bacnetip.WithNodePromiscuous(true)) + if err != nil { + return nil, errors.Wrap(err, "error creating node") + } + s.log.Debug().Stringer("node", s.node).Msg("node") + + // bind the node + err = bacnetip.Bind(s.log, s, s.node) + if err != nil { + return nil, errors.Wrap(err, "error binding node") + } + return s, nil +} + +func (s *SnifferNode) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("request") + return errors.New("sniffers don't request") +} + +func (s *SnifferNode) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("confirmation") + pdu := args.Get0PDU() + + // it's and NPDU + npdu := pdu.GetMessage().(model.NPDU) + + // filter out network layer traffic if there is any, probably not + if nlm := npdu.GetNlm(); nlm != nil { + s.log.Debug().Stringer("nlm", nlm).Msg("network message") + return nil + } + + // decode as generic APDU + apdu := npdu.GetApdu() + + // TODO: not sure what to do here + /* + # "lift" the source and destination address + if npdu.npduSADR: + apdu.pduSource = npdu.npduSADR + else: + apdu.pduSource = npdu.pduSource + if npdu.npduDADR: + apdu.pduDestination = npdu.npduDADR + else: + apdu.pduDestination = npdu.pduDestination + */ + _ = apdu + + // TODO: print etc + + return nil +} + +type ApplicationStateMachineRequirements interface { + Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +type ApplicationStateMachine struct { + ApplicationStateMachineRequirements + *bacnetip.Application + tests.StateMachine + + address *bacnetip.Address + asap *bacnetip.ApplicationServiceAccessPoint + smap *bacnetip.StateMachineAccessPoint + nsap *bacnetip.NetworkServiceAccessPoint + nse *_NetworkServiceElement + node *bacnetip.Node + + confirmedPrivateResult string + + log zerolog.Logger +} + +func NewApplicationStateMachine(localLog zerolog.Logger, localDevice *bacnetip.LocalDeviceObject, vlan *bacnetip.Network, applicationStateMachineRequirements ApplicationStateMachineRequirements) (*ApplicationStateMachine, error) { + a := &ApplicationStateMachine{ + ApplicationStateMachineRequirements: applicationStateMachineRequirements, + log: localLog, + } + + // build and address and save it + _, instance := bacnetip.ObjectIdentifierStringToTuple(localDevice.ObjectIdentifier) + var err error + a.address, err = bacnetip.NewAddress(a.log, instance) + if err != nil { + return nil, errors.Wrap(err, "error creating address") + } + a.log.Debug().Stringer("address", a.address).Msg("address") + + // continue with initialization + a.Application, err = bacnetip.NewApplication(a.log, localDevice) + if err != nil { + return nil, errors.Wrap(err, "error creating application io controller") + } + var init func() + a.StateMachine, init = tests.NewStateMachine(a.log, a, tests.WithStateMachineName(localDevice.ObjectName)) + init() + + // include a application decoder + a.asap, err = bacnetip.NewApplicationServiceAccessPoint(a.log) + if err != nil { + return nil, errors.Wrap(err, "error creating application service access point") + } + + // pass the device object to the state machine access point so it + // can know if it should support segmentation + + // the segmentation state machines need access to the same device + // information cache as the application + deviceInfoCache := a.GetDeviceInfoCache() + a.smap, err = bacnetip.NewStateMachineAccessPoint(a.log, localDevice, bacnetip.WithStateMachineAccessPointDeviceInfoCache(deviceInfoCache)) + if err != nil { + return nil, errors.Wrap(err, "error creating state machine access point") + } + + // a network service access point will be needed + a.nsap, err = bacnetip.NewNetworkServiceAccessPoint(a.log) + if err != nil { + return nil, errors.Wrap(err, "error creating network service access point") + } + + // give the NSAP a generic network layer service element + a.nse, err = new_NetworkServiceElement(a.log) + if err != nil { + return nil, errors.Wrap(err, "error creating network service element") + } + err = bacnetip.Bind(a.log, a.nse, a.nsap) + if err != nil { + return nil, errors.Wrap(err, "error binding network service element") + } + + // bind the top layers + err = bacnetip.Bind(a.log, a, a.asap, a.smap, a.nsap) + if err != nil { + return nil, errors.Wrap(err, "error binding top layers") + } + + // create a node, added to the network + a.node, err = bacnetip.NewNode(a.log, a.address, vlan) + if err != nil { + return nil, errors.Wrap(err, "error creating node") + } + + // bind the network service to the node, no network number + err = a.nsap.Bind(a.node, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error binding nsap") + } + return a, nil +} + +func (a *ApplicationStateMachine) String() string { + return "ApplicationStateMachine" //TODO: +} + +func (a *ApplicationStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send") + + // send the apdu down the stack + return a.Request(args, kwargs) +} + +func (a *ApplicationStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + + // let the state machine know the request was received + err := a.Receive(args, bacnetip.NoKWArgs) + if err != nil { + return errors.Wrap(err, "error receiving indication") + } + + // allow the application to process it + return a.ApplicationStateMachineRequirements.Indication(args, kwargs) +} + +func (a *ApplicationStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation") + + // forward the confirmation to the state machine + return a.Receive(args, kwargs) +} + +// TODO +func (a *ApplicationStateMachine) doConfirmedPrivateTransferRequest(_ struct{}) { + // TODO: who calls that? +} + +type TODOWHATTODOWITHTHAT struct { +} + +func (T TODOWHATTODOWITHTHAT) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + //TODO implement me + panic("implement me") +} + +func (T TODOWHATTODOWITHTHAT) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + //TODO implement me + panic("implement me") +} + +func SegmentationTest(t *testing.T, prefix string, cLen, sLen int) { + tests.LockGlobalTimeMachine(t) + testingLogger := testutils.ProduceTestingLogger(t) + tests.NewGlobalTimeMachine(testingLogger) + + // client device object + octets206 := model.MaxApduLengthAccepted_NUM_OCTETS_206 + segmentation := model.BACnetSegmentation_SEGMENTED_BOTH + maxSegmentsAccepted := model.MaxSegmentsAccepted_NUM_SEGMENTS_04 + tdDeviceObject := &bacnetip.LocalDeviceObject{ + ObjectName: "td", + ObjectIdentifier: "device:10", + MaximumApduLengthAccepted: &octets206, + SegmentationSupported: &segmentation, + MaxSegmentsAccepted: &maxSegmentsAccepted, + VendorIdentifier: 999, + } + + // server device object + maxSegmentsAccepted = model.MaxSegmentsAccepted_NUM_SEGMENTS_64 + iutDeviceObject := &bacnetip.LocalDeviceObject{ + ObjectName: "td", + ObjectIdentifier: "device:10", + MaximumApduLengthAccepted: &octets206, + SegmentationSupported: &segmentation, + MaxSegmentsAccepted: &maxSegmentsAccepted, + VendorIdentifier: 999, + } + + // create a network + anet, err := NewApplicationNetwork(testingLogger, new(TODOWHATTODOWITHTHAT), tdDeviceObject, iutDeviceObject) + require.NoError(t, err) + + // tell the device info cache of the client about the server + if false { + // TODO: what + //anet.td.GetDeviceInfoCache().GetDeviceInfo(anet.iut.address.String()) + // # update the rest of the values + // iut_device_info.maxApduLengthAccepted = iut_device_object.maxApduLengthAccepted + // iut_device_info.segmentationSupported = iut_device_object.segmentationSupported + // iut_device_info.vendorID = iut_device_object.vendorIdentifier + // iut_device_info.maxSegmentsAccepted = iut_device_object.maxSegmentsAccepted + // iut_device_info.maxNpduLength = s_ndpu_len + } + + // tell the device info cache of the server device about the client + if false { + // TODO: what + //anet.iut.GetDeviceInfoCache().GetDeviceInfo(anet.td.address.String()) + // # update the rest of the values + // td_device_info.maxApduLengthAccepted = td_device_object.maxApduLengthAccepted + // td_device_info.segmentationSupported = td_device_object.segmentationSupported + // td_device_info.vendorID = td_device_object.vendorIdentifier + // td_device_info.maxSegmentsAccepted = td_device_object.maxSegmentsAccepted + // td_device_info.maxNpduLength = c_ndpu_len + } + + // build a request string + var requestString string + if cLen != 0 { + requestString = utils.RandomString(cLen) + } + + if sLen != 0 { + anet.iut.confirmedPrivateResult = utils.RandomString(sLen) + } + + wrapRequestString := func() ( + numberOfDataElements model.BACnetApplicationTagUnsignedInteger, + data []model.BACnetConstructedDataElement, + openingTag model.BACnetOpeningTag, + peekedTagHeader model.BACnetTagHeader, + closingTag model.BACnetClosingTag, + tagNumber uint8, + arrayIndexArgument model.BACnetTagPayloadUnsignedInteger, + ) { + numberOfDataElements = model.CreateBACnetApplicationTagUnsignedInteger(1) + elementArgCreator := func() ( + peekedTagHeader model.BACnetTagHeader, + applicationTag model.BACnetApplicationTag, + contextTag model.BACnetContextTag, + constructedData model.BACnetConstructedData, + objectTypeArgument model.BACnetObjectType, + propertyIdentifierArgument model.BACnetPropertyIdentifier, + arrayIndexArgument model.BACnetTagPayloadUnsignedInteger, + ) { + peekedTagHeader = model.CreateBACnetTagHeaderBalanced(false, 0, 0) + applicationTag = model.CreateBACnetApplicationTagCharacterString(model.BACnetCharacterEncoding_ISO_8859_1, requestString) + return + } + data = []model.BACnetConstructedDataElement{ + model.NewBACnetConstructedDataElement(elementArgCreator()), + } + openingTag = model.CreateBACnetOpeningTag(2) + peekedTagHeader = model.CreateBACnetTagHeaderBalanced(false, 2, 2) + closingTag = model.CreateBACnetClosingTag(2) + return + } + + cpt := model.NewBACnetConfirmedServiceRequestConfirmedPrivateTransfer( + model.CreateBACnetVendorIdContextTagged(0, 999), + model.CreateBACnetContextTagUnsignedInteger(1, 1), + model.NewBACnetConstructedDataUnspecified(wrapRequestString()), + 0, + ) + + apdu := model.NewAPDUConfirmedRequest( + false, + false, + true, + maxSegmentsAccepted, + model.MaxApduLengthAccepted_NUM_OCTETS_1024, + 0, + nil, + nil, + cpt, + nil, + nil, + 0) + + rq := bacnetip.NewPDU(apdu) + + var trq model.BACnetServiceAckConfirmedPrivateTransfer + // send the request, get it acked + anet.td.GetStartState().Doc(prefix+"-0"). + Send(rq, nil).Doc(prefix+"-1"). + Receive(trq, nil).Doc(prefix + "-2"). + Success("") + + // no IUT application layer matching + anet.iut.GetStartState().Success("") + + // run the group + err = anet.Run(9) + assert.NoError(t, err) +} + +func Test1(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-1", 0, 0) +} + +func Test2(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-2", 10, 0) +} + +func Test3(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-3", 100, 0) +} + +func Test4(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-4", 200, 0) +} + +func Test5(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-5", 0, 10) +} + +func Test6(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-6", 0, 200) +} + +func Test7(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-7", 300, 0) +} + +func Test8(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-8", 300, 300) +} + +func Test9(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + SegmentationTest(t, "7-9", 600, 600) +} diff --git a/plc4go/internal/bacnetip/tests/test_service/helpers.go b/plc4go/internal/bacnetip/tests/test_service/helpers.go new file mode 100644 index 00000000000..997c11987c3 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/helpers.go @@ -0,0 +1,492 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +import ( + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// This struct turns off the deferred startup function call that broadcasts I-Am-Router-To-Network and Network-Number-Is +// +// messages. +type _NetworkServiceElement struct { + *bacnetip.NetworkServiceElement +} + +func new_NetworkServiceElement(localLog zerolog.Logger) (*_NetworkServiceElement, error) { + n := &_NetworkServiceElement{} + var err error + n.NetworkServiceElement, err = bacnetip.NewNetworkServiceElement(localLog, nil, true) + if err != nil { + return nil, errors.Wrap(err, "error creating network service element") + } + return n, nil +} + +type ApplicationNetworkRequirements interface { + Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error + Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +type ApplicationNetwork struct { + ApplicationNetworkRequirements + *tests.StateMachineGroup + + trafficLog *tests.TrafficLog + vlan *bacnetip.Network + tdDeviceObject *bacnetip.LocalDeviceObject + td *ApplicationStateMachine + iutDeviceObject *bacnetip.LocalDeviceObject + iut *ApplicationStateMachine + + log zerolog.Logger +} + +func NewApplicationNetwork(localLog zerolog.Logger, applicationNetworkRequirements ApplicationNetworkRequirements) (*ApplicationNetwork, error) { + a := &ApplicationNetwork{ + ApplicationNetworkRequirements: applicationNetworkRequirements, + log: localLog, + } + a.StateMachineGroup = tests.NewStateMachineGroup(localLog) + + // Reset the time machine + tests.ResetTimeMachine(time.Time{}) + + // Create a traffic log + a.trafficLog = new(tests.TrafficLog) + + // make a little LAN + a.vlan = bacnetip.NewNetwork(localLog, bacnetip.WithNetworkBroadcastAddress(bacnetip.NewLocalBroadcast(nil)), bacnetip.WithNetworkTrafficLogger(a.trafficLog)) + + // test device object + octets1024 := model.MaxApduLengthAccepted_NUM_OCTETS_1024 + segmentation := model.BACnetSegmentation_NO_SEGMENTATION + a.tdDeviceObject = &bacnetip.LocalDeviceObject{ + ObjectName: "td", + ObjectIdentifier: "device:10", + MaximumApduLengthAccepted: &octets1024, + SegmentationSupported: &segmentation, + VendorIdentifier: 999, + } + + // test device + var err error + a.td, err = NewApplicationStateMachine(localLog, a.tdDeviceObject, a.vlan, a) + if err != nil { + return nil, errors.Wrap(err, "error building application state machine") + } + a.Append(a.td) + + // implementation under test device object + octets1024 = model.MaxApduLengthAccepted_NUM_OCTETS_1024 + segmentation = model.BACnetSegmentation_NO_SEGMENTATION + a.iutDeviceObject = &bacnetip.LocalDeviceObject{ + ObjectName: "iut", + ObjectIdentifier: "device:20", + MaximumApduLengthAccepted: &octets1024, + SegmentationSupported: &segmentation, + VendorIdentifier: 999, + } + + // implementation under test + a.iut, err = NewApplicationStateMachine(localLog, a.iutDeviceObject, a.vlan, a) + if err != nil { + return nil, errors.Wrap(err, "error building application state machine") + } + a.Append(a.iut) + + return a, nil +} + +func (a *ApplicationNetwork) Run(timeLimit time.Duration) error { + if timeLimit == 0 { + timeLimit = 60 * time.Second + } + a.log.Debug().Dur("timeLimit", timeLimit).Msg("run") + + // Run the group + err := a.StateMachineGroup.Run() + if err != nil { + return errors.Wrap(err, "error running state machine group") + } + a.log.Trace().Msg("group running") + + // run it for some time + tests.RunTimeMachine(a.log, timeLimit, time.Time{}) + if a.log.Debug().Enabled() { + a.log.Debug().Msg("time machine finished") + for _, machine := range a.GetStateMachines() { + a.log.Debug().Stringer("machine", machine).Msg("machine") + for _, entry := range machine.GetTransactionLog() { + a.log.Debug().Str("entry", entry).Msg("transaction log entry") + } + } + + a.trafficLog.Dump(a._debug) + } + + // check for success + success, failed := a.CheckForSuccess() + if !success { + return errors.New("application network did not succeed") + } + _ = failed + + return nil +} + +func (a *ApplicationNetwork) _debug(format string, args bacnetip.Args) { + a.log.Debug().Msgf(format, args) +} + +type SnifferNode struct { + *bacnetip.Client + + name string + address *bacnetip.Address + node *bacnetip.Node + + log zerolog.Logger +} + +func NewSnifferNode(localLog zerolog.Logger, vlan *bacnetip.Network) (*SnifferNode, error) { + s := &SnifferNode{ + name: "sniffer", + log: localLog, + } + s.address, _ = bacnetip.NewAddress(localLog) + var err error + s.Client, err = bacnetip.NewClient(localLog, s) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + + // create a promiscuous node, added to the network + s.node, err = bacnetip.NewNode(localLog, s.address, vlan, bacnetip.WithNodePromiscuous(true)) + if err != nil { + return nil, errors.Wrap(err, "error creating node") + } + s.log.Debug().Stringer("node", s.node).Msg("node") + + // bind the node + err = bacnetip.Bind(s.log, s, s.node) + if err != nil { + return nil, errors.Wrap(err, "error binding node") + } + return s, nil +} + +func (s *SnifferNode) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("request") + return errors.New("sniffers don't request") +} + +func (s *SnifferNode) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("confirmation") + pdu := args.Get0PDU() + + // it's and NPDU + npdu := pdu.GetMessage().(model.NPDU) + + // filter out network layer traffic if there is any, probably not + if nlm := npdu.GetNlm(); nlm != nil { + s.log.Debug().Stringer("nlm", nlm).Msg("network message") + return nil + } + + // decode as generic APDU + apdu := npdu.GetApdu() + + // TODO: not sure what to do here + /* + # "lift" the source and destination address + if npdu.npduSADR: + apdu.pduSource = npdu.npduSADR + else: + apdu.pduSource = npdu.pduSource + if npdu.npduDADR: + apdu.pduDestination = npdu.npduDADR + else: + apdu.pduDestination = npdu.pduDestination + */ + _ = apdu + + // TODO: print etc + + return nil +} + +type SnifferStateMachine struct { + *bacnetip.Client + tests.StateMachine + + name string + address *bacnetip.Address + node *bacnetip.Node + + log zerolog.Logger +} + +func NewSnifferStateMachine(localLog zerolog.Logger, vlan *bacnetip.Network) (*SnifferStateMachine, error) { + s := &SnifferStateMachine{ + name: "sniffer", + log: localLog, + } + s.address, _ = bacnetip.NewAddress(localLog) + + // continue with initialization + var err error + s.Client, err = bacnetip.NewClient(localLog, s) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + var init func() + s.StateMachine, init = tests.NewStateMachine(localLog, s) + init() + + // create a promiscuous node, added to the network + s.node, err = bacnetip.NewNode(localLog, s.address, vlan, bacnetip.WithNodePromiscuous(true)) + if err != nil { + return nil, errors.Wrap(err, "error creating node") + } + s.log.Debug().Stringer("node", s.node).Msg("node") + + // bind the node + err = bacnetip.Bind(s.log, s, s.node) + if err != nil { + return nil, errors.Wrap(err, "error binding node") + } + return s, nil +} + +func (s *SnifferStateMachine) Sends(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("request") + return errors.New("sniffers don't send") +} + +func (s *SnifferStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("confirmation") + pdu := args.Get0PDU() + + // it's and NPDU + npdu := pdu.GetMessage().(model.NPDU) + + // filter out network layer traffic if there is any, probably not + if nlm := npdu.GetNlm(); nlm != nil { + s.log.Debug().Stringer("nlm", nlm).Msg("network message") + return nil + } + + // decode as generic APDU + apdu := npdu.GetApdu() + + // TODO: not sure what to do here + /* + # "lift" the source and destination address + if npdu.npduSADR: + apdu.pduSource = npdu.npduSADR + else: + apdu.pduSource = npdu.pduSource + if npdu.npduDADR: + apdu.pduDestination = npdu.npduDADR + else: + apdu.pduDestination = npdu.pduDestination + */ + _ = apdu + + // TODO: print etc + + // pass to the state machine + return s.Receive(args, bacnetip.NoKWArgs) +} + +func (s *SnifferStateMachine) String() string { + return "SnifferStateMachine" //TODO +} + +type ApplicationStateMachineRequirements interface { + Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error + Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +type ApplicationStateMachine struct { + ApplicationStateMachineRequirements + *bacnetip.ApplicationIOController + tests.StateMachine + + address *bacnetip.Address + asap *bacnetip.ApplicationServiceAccessPoint + smap *bacnetip.StateMachineAccessPoint + nsap *bacnetip.NetworkServiceAccessPoint + nse *_NetworkServiceElement + node *bacnetip.Node + + log zerolog.Logger +} + +func NewApplicationStateMachine(localLog zerolog.Logger, localDevice *bacnetip.LocalDeviceObject, vlan *bacnetip.Network, applicationStateMachineRequirements ApplicationStateMachineRequirements) (*ApplicationStateMachine, error) { + a := &ApplicationStateMachine{ + ApplicationStateMachineRequirements: applicationStateMachineRequirements, + log: localLog, + } + + // build and address and save it + _, instance := bacnetip.ObjectIdentifierStringToTuple(localDevice.ObjectIdentifier) + var err error + a.address, err = bacnetip.NewAddress(a.log, instance) + if err != nil { + return nil, errors.Wrap(err, "error creating address") + } + a.log.Debug().Stringer("address", a.address).Msg("address") + + // continue with initialization + a.ApplicationIOController, err = bacnetip.NewApplicationIOController(a.log, localDevice) + if err != nil { + return nil, errors.Wrap(err, "error creating application io controller") + } + var init func() + a.StateMachine, init = tests.NewStateMachine(a.log, a, tests.WithStateMachineName(localDevice.ObjectName)) + init() + + // include a application decoder + a.asap, err = bacnetip.NewApplicationServiceAccessPoint(a.log) + if err != nil { + return nil, errors.Wrap(err, "error creating application service access point") + } + + // pass the device object to the state machine access point so it + // can know if it should support segmentation + + // the segmentation state machines need access to the same device + // information cache as the application + deviceInfoCache := a.GetDeviceInfoCache() + a.smap, err = bacnetip.NewStateMachineAccessPoint(a.log, localDevice, bacnetip.WithStateMachineAccessPointDeviceInfoCache(deviceInfoCache)) + if err != nil { + return nil, errors.Wrap(err, "error creating state machine access point") + } + + // a network service access point will be needed + a.nsap, err = bacnetip.NewNetworkServiceAccessPoint(a.log) + if err != nil { + return nil, errors.Wrap(err, "error creating network service access point") + } + + // give the NSAP a generic network layer service element + a.nse, err = new_NetworkServiceElement(a.log) + if err != nil { + return nil, errors.Wrap(err, "error creating network service element") + } + err = bacnetip.Bind(a.log, a.nse, a.nsap) + if err != nil { + return nil, errors.Wrap(err, "error binding network service element") + } + + // bind the top layers + err = bacnetip.Bind(a.log, a, a.asap, a.smap, a.nsap) + if err != nil { + return nil, errors.Wrap(err, "error binding top layers") + } + + // create a node, added to the network + a.node, err = bacnetip.NewNode(a.log, a.address, vlan) + if err != nil { + return nil, errors.Wrap(err, "error creating node") + } + + // bind the network service to the node, no network number + err = a.nsap.Bind(a.node, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error binding nsap") + } + return a, nil +} + +func (a *ApplicationStateMachine) String() string { + return "ApplicationStateMachine" //TODO: +} + +func (a *ApplicationStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send") + + pdu := args.Get0PDU() + // build a IOCB to wrap the request + iocb, err := bacnetip.NewIOCB(a.log, pdu, nil) + if err != nil { + return errors.Wrap(err, "error creating iocb") + } + return a.Request(bacnetip.NewArgs(iocb), bacnetip.NoKWArgs) +} + +func (a *ApplicationStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + + // let the state machine know the request was received + err := a.Receive(args, bacnetip.NoKWArgs) + if err != nil { + return errors.Wrap(err, "error receiving indication") + } + + // allow the application to process it + return a.ApplicationStateMachineRequirements.Indication(args, kwargs) +} + +func (a *ApplicationStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation") + + // forward the confirmation to the state machine + err := a.Receive(args, bacnetip.NoKWArgs) + if err != nil { + return errors.Wrap(err, "error receiving indication") + } + + // allow the application to process it + return a.ApplicationStateMachineRequirements.Confirmation(args, kwargs) +} + +type COVTestClientServicesRequirements interface { +} + +type COVTestClientServices struct { + COVTestClientServicesRequirements + *bacnetip.Capability + + log zerolog.Logger +} + +func (c *COVTestClientServices) doConfirmedCOVNotificationRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + c.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("doConfirmedCOVNotificationRequest") + + panic("TODO: implement me") // TODO:implement me + return nil +} + +func (c *COVTestClientServices) doUnconfirmedCOVNotificationRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + c.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("doUnconfirmedCOVNotificationRequest") + + panic("TODO: implement me") // TODO:implement me + return nil +} diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_test.go new file mode 100644 index 00000000000..3939586704e --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_test.go @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +import ( + "testing" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/service" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TODOWhatToDoWithThatWiringWrongQuestionMark struct { +} + +func (T TODOWhatToDoWithThatWiringWrongQuestionMark) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + //TODO implement me + panic("implement me") +} + +func (T TODOWhatToDoWithThatWiringWrongQuestionMark) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + //TODO implement me + panic("implement me") +} + +func TestBasic(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + tests.NewGlobalTimeMachine(testingLogger) + + // create a network + anet, err := NewApplicationNetwork(testingLogger, new(TODOWhatToDoWithThatWiringWrongQuestionMark)) + require.NoError(t, err) + + // add the service capability + anet.iut.AddCapability(new(service.ChangeOfValuesServices)) + + // all start states are successful + anet.td.GetStartState().Success("") + anet.iut.GetStartState().Success("") + + // run the group + err = anet.Run(0) + assert.NoError(t, err) +} diff --git a/plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go b/plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_service/test_device_test.go b/plc4go/internal/bacnetip/tests/test_service/test_device_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_device_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_service/test_file_test.go b/plc4go/internal/bacnetip/tests/test_service/test_file_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_file_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_service/test_object_test.go b/plc4go/internal/bacnetip/tests/test_service/test_object_test.go new file mode 100644 index 00000000000..d7fae209dcd --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_service/test_object_test.go @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_service + +// TODO: implement diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go new file mode 100644 index 00000000000..4e2c30d0038 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_utilities + +import ( + "testing" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClientStateMachine(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + // create a client state machine, trapped server, and bind them together + client, err := tests.NewClientStateMachine(testingLogger) + require.NoError(t, err) + server, err := tests.NewTrappedServer(testingLogger, nil) + require.NoError(t, err) + err = bacnetip.Bind(testingLogger, client, server) + require.NoError(t, err) + + // make pdu object + pdu := bacnetip.NewPDU(tests.NewDummyMessage()) + + // make a send transition from start to success, run the machine + client.GetStartState().Send(pdu, nil).Success("") + + // run the machine + err = client.Run() + assert.Nil(t, err) + + // check for success + assert.False(t, client.IsRunning()) + assert.True(t, client.GetCurrentState().IsSuccessState()) + + // make sure the pdu was sent + assert.Equal(t, pdu, server.GetIndicationReceived()) + + // check the transaction log + assert.Len(t, client.GetTransactionLog(), 1) + assert.Contains(t, client.GetTransactionLog()[0], pdu.String()) +} diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go new file mode 100644 index 00000000000..e938f5c1dee --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_utilities + +import ( + "testing" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestServerStateMachine(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + // create a client state machine, trapped server, and bind them together + client, err := tests.NewTrappedClient(testingLogger, nil) + require.NoError(t, err) + server, err := tests.NewServerStateMachine(testingLogger) + require.NoError(t, err) + err = bacnetip.Bind(testingLogger, client, server) + require.NoError(t, err) + + // make pdu object + pdu := bacnetip.NewPDU(tests.NewDummyMessage()) + + // make a send transition from start to success, run the machine + server.GetStartState().Send(pdu, nil).Success("") + + // run the machine + err = server.Run() + assert.Nil(t, err) + + // check for success + assert.False(t, server.IsRunning()) + assert.True(t, server.GetCurrentState().IsSuccessState()) + + // make sure the pdu was sent + assert.Equal(t, pdu, client.GetConfirmationReceived()) + + // check the transaction log + assert.Len(t, server.GetTransactionLog(), 1) + assert.Contains(t, server.GetTransactionLog()[0], pdu.String()) +} diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go new file mode 100644 index 00000000000..b12faba8ea0 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_utilities + +import ( + "testing" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + "github.com/stretchr/testify/suite" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type EchoAccessPointRequirements interface { + SapResponse(args bacnetip.Args, kwArgs bacnetip.KWArgs) error +} + +type EchoAccessPoint struct { + *bacnetip.ServiceAccessPoint + + echoAccessPointRequirements EchoAccessPointRequirements + + log zerolog.Logger +} + +func NewEchoAccessPoint(localLog zerolog.Logger, serviceAccessPoint *bacnetip.ServiceAccessPoint) *EchoAccessPoint { + e := &EchoAccessPoint{ + ServiceAccessPoint: serviceAccessPoint, + + log: localLog, + } + return e +} + +func (e *EchoAccessPoint) SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapIndication") + return e.echoAccessPointRequirements.SapResponse(args, bacnetip.NoKWArgs) +} + +func (e *EchoAccessPoint) SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapConfirmation") + return nil +} + +type TrappedEchoAccessPoint struct { + *bacnetip.ServiceAccessPoint + *EchoAccessPoint + *tests.TrappedServiceAccessPoint + + log zerolog.Logger +} + +func NewTrappedEchoAccessPoint(localLog zerolog.Logger) (*TrappedEchoAccessPoint, error) { + t := &TrappedEchoAccessPoint{} + var err error + t.ServiceAccessPoint, err = bacnetip.NewServiceAccessPoint(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "Error creating service access point") + } + t.EchoAccessPoint = NewEchoAccessPoint(localLog, t.ServiceAccessPoint) + t.TrappedServiceAccessPoint, err = tests.NewTrappedServiceAccessPoint(localLog, t.EchoAccessPoint) + if err != nil { + return nil, errors.Wrap(err, "error creating trapped service access point") + } + t.EchoAccessPoint.echoAccessPointRequirements = t // TODO: isn't multi-inheritance easy to follow? At this point it is pretty confusing + return t, nil +} + +func (t *TrappedEchoAccessPoint) SapRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedServiceAccessPoint.SapRequest(args, kwargs) +} + +func (t *TrappedEchoAccessPoint) SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedServiceAccessPoint.SapIndication(args, kwargs) +} + +func (t *TrappedEchoAccessPoint) SapResponse(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedServiceAccessPoint.SapResponse(args, kwargs) +} + +func (t *TrappedEchoAccessPoint) SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedServiceAccessPoint.SapConfirmation(args, kwargs) +} + +func (t *TrappedEchoAccessPoint) String() string { + return "TrappedEchoAccessPoint" +} + +type EchoServiceElementRequirements interface { + Response(args bacnetip.Args, kwArgs bacnetip.KWArgs) error +} + +type EchoServiceElement struct { + *bacnetip.ApplicationServiceElement + + echoServiceElementRequirements EchoServiceElementRequirements + + log zerolog.Logger +} + +func NewEchoServiceElement(localLog zerolog.Logger, applicationServiceElement *bacnetip.ApplicationServiceElement) *EchoServiceElement { + e := &EchoServiceElement{ + ApplicationServiceElement: applicationServiceElement, + log: localLog, + } + return e +} + +func (e *EchoServiceElement) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + return e.echoServiceElementRequirements.Response(args, bacnetip.NoKWArgs) +} + +func (e *EchoServiceElement) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation") + return nil +} + +func (e *EchoServiceElement) String() string { + return "EchoServiceElement" // TODO: fill with some useful +} + +type TrappedEchoServiceElement struct { + *bacnetip.ApplicationServiceElement + *EchoServiceElement + *tests.TrappedApplicationServiceElement +} + +func NewTrappedEchoServiceElement(localLog zerolog.Logger) (*TrappedEchoServiceElement, error) { + t := &TrappedEchoServiceElement{} + var err error + t.ApplicationServiceElement, err = bacnetip.NewApplicationServiceElement(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "Error creating service access point") + } + t.EchoServiceElement = NewEchoServiceElement(localLog, t.ApplicationServiceElement) + t.TrappedApplicationServiceElement, err = tests.NewTrappedApplicationServiceElement(localLog, t.EchoServiceElement) + if err != nil { + return nil, errors.Wrap(err, "error creating trapped application service element") + } + t.EchoServiceElement.echoServiceElementRequirements = t // TODO: isn't multi-inheritance easy to follow? At this point it is pretty confusing + return t, nil +} + +func (t *TrappedEchoServiceElement) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedApplicationServiceElement.Request(args, kwargs) +} + +func (t *TrappedEchoServiceElement) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedApplicationServiceElement.Indication(args, kwargs) +} + +func (t *TrappedEchoServiceElement) Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedApplicationServiceElement.Response(args, kwargs) +} + +func (t *TrappedEchoServiceElement) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + return t.TrappedApplicationServiceElement.Confirmation(args, kwargs) +} + +func (t *TrappedEchoServiceElement) String() string { + return "TrappedEchoServiceElement" +} + +type TestApplicationSuite struct { + suite.Suite + + sap *TrappedEchoAccessPoint + ase *TrappedEchoServiceElement + + log zerolog.Logger +} + +func (suite *TestApplicationSuite) SetupSuite() { + suite.log = testutils.ProduceTestingLogger(suite.T()) + + var err error + suite.sap, err = NewTrappedEchoAccessPoint(suite.log) + suite.Require().NoError(err) + + suite.ase, err = NewTrappedEchoServiceElement(suite.log) + suite.Require().NoError(err) + + err = bacnetip.Bind(suite.log, suite.ase, suite.sap) + suite.Require().NoError(err) +} + +func (suite *TestApplicationSuite) TearDownSuite() { +} + +func (suite *TestApplicationSuite) TestSapRequest() { + // make a pdu + pdu := bacnetip.NewPDU(tests.NewDummyMessage()) + + // service access point is going to request something + err := suite.sap.SapRequest(bacnetip.NewArgs(pdu), bacnetip.NoKWArgs) + suite.Assert().NoError(err) + + // make sure the request was sent and received + suite.Equal(pdu, suite.sap.GetSapRequestSent()) + suite.Equal(pdu, suite.ase.GetIndicationReceived()) + + // make sure the echo response was sent and received + suite.Equal(pdu, suite.ase.GetResponseSent()) + suite.Equal(pdu, suite.sap.GetSapConfirmationReceived()) +} + +func (suite *TestApplicationSuite) TestAseRequest() { + // make a pdu + pdu := bacnetip.NewPDU(tests.NewDummyMessage()) + + // service access point is going to request something + err := suite.ase.Request(bacnetip.NewArgs(pdu), bacnetip.NoKWArgs) + suite.Assert().NoError(err) + + // make sure the request was sent and received + suite.Equal(pdu, suite.ase.GetRequestSent()) + suite.Equal(pdu, suite.sap.GetSapIndicationReceived()) + + // make sure the echo response was sent and received + suite.Equal(pdu, suite.sap.GetSapResponseSent()) + suite.Equal(pdu, suite.ase.GetConfirmationReceived()) +} + +func TestApplicationService(t *testing.T) { + suite.Run(t, new(TestApplicationSuite)) +} diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go new file mode 100644 index 00000000000..8c2ffbbcdaa --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go @@ -0,0 +1,666 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_utilities + +import ( + "fmt" + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + readWriteModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" + "github.com/apache/plc4x/plc4go/spi" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TPDU struct { + x []byte + a, b int +} + +func (t TPDU) X() []byte { + return t.x +} + +func (t TPDU) A() int { + return t.a +} + +func (t TPDU) B() int { + return t.b +} + +func (t TPDU) String() string { + content := "" + if t.x != nil { + content += fmt.Sprintf(" x=%v", t.x) + } + if t.a != 0 { + content += fmt.Sprintf(" a=%v", t.a) + } + if t.b != 0 { + content += fmt.Sprintf(" b=%v", t.b) + } + return fmt.Sprintf("", content) +} + +func (t TPDU) GetMessage() spi.Message { + panic("implement me") +} + +func (t TPDU) GetPDUSource() *bacnetip.Address { + panic("implement me") +} + +func (t TPDU) SetPDUSource(source *bacnetip.Address) { + panic("implement me") +} + +func (t TPDU) GetPDUDestination() *bacnetip.Address { + panic("implement me") +} + +func (t TPDU) SetPDUDestination(address *bacnetip.Address) { + panic("implement me") +} + +func (t TPDU) GetExpectingReply() bool { + panic("implement me") +} + +func (t TPDU) GetNetworkPriority() readWriteModel.NPDUNetworkPriority { + panic("implement me") +} + +func (t TPDU) DeepCopy() bacnetip.PDU { + panic("implement me") +} + +type Anon struct { + TPDU +} + +func TestMatchPdu(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + tpdu := TPDU{x: []byte{1}} + anon := Anon{TPDU{x: []byte("Anon")}} + + // no criteria passes + assert.True(t, tests.MatchPdu(testingLogger, tpdu, nil, nil)) + assert.True(t, tests.MatchPdu(testingLogger, anon, nil, nil)) + + // matching/not matching types + assert.True(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, nil)) + assert.False(t, tests.MatchPdu(testingLogger, tpdu, Anon{}, nil)) + // Note the other testcase is irrelevant as we don't have dynamic types + + // matching/not matching attributes + assert.True(t, tests.MatchPdu(testingLogger, tpdu, nil, map[string]any{"x": []byte{1}})) + assert.False(t, tests.MatchPdu(testingLogger, tpdu, nil, map[string]any{"x": []byte{2}})) + assert.False(t, tests.MatchPdu(testingLogger, tpdu, nil, map[string]any{"y": []byte{1}})) + assert.False(t, tests.MatchPdu(testingLogger, anon, nil, map[string]any{"x": []byte{1}})) + + // matching/not matching types and attributes + assert.True(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, map[string]any{"x": []byte{1}})) + assert.False(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, map[string]any{"x": []byte{2}})) + assert.False(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, map[string]any{"y": []byte{1}})) + assert.False(t, tests.MatchPdu(testingLogger, anon, Anon{}, map[string]any{"x": []byte{1}})) +} + +func TestState(t *testing.T) { + t.Run("test_state_doc", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // change the doc string + ts := tests.NewState(testingLogger, nil, "") + ns := ts.Doc("test state") + + assert.Equal(t, "test state", ts.DocString()) + assert.Same(t, ts, ns) + }) + t.Run("test_success", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + ts := tests.NewState(testingLogger, nil, "") + ns := ts.Success("") + assert.True(t, ts.IsSuccessState()) + assert.Same(t, ts, ns) + + assert.Panics(t, func() { + ts.Success("") + }) + assert.Panics(t, func() { + ts.Fail("") + }) + }) + t.Run("test_state_fail", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + ts := tests.NewState(testingLogger, nil, "") + ns := ts.Fail("") + assert.True(t, ts.IsFailState()) + assert.Same(t, ts, ns) + + assert.Panics(t, func() { + ts.Success("") + }) + assert.Panics(t, func() { + ts.Fail("") + }) + }) +} + +func TestStateMachine(t *testing.T) { + t.Run("test_state_machine_run", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine //TODO: fix nil requirement + var init func() + tsm, init := tests.NewStateMachine(testingLogger, nil) + init() + + // run the machine + err := tsm.Run() + assert.NoError(t, err) + + assert.True(t, tsm.IsRunning()) + assert.Same(t, tsm.GetStartState(), tsm.GetCurrentState()) + }) + t.Run("test_state_machine_success", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // make the start state a sucess + tsm.GetStartState().Success("") + + // run the machine + err := tsm.Run() + assert.NoError(t, err) + + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + }) + t.Run("test_state_machine_fail", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // make the start state a sucess + tsm.GetStartState().Fail("") + + // run the machine + err := tsm.Run() + assert.NoError(t, err) + + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsFailState()) + }) + t.Run("test_state_machine_send", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // Make a pdu object + pdu := bacnetip.NewPDU(nil) + + // make a send transition from start to success, run the machine + tsm.GetStartState().Send(pdu, nil).Success("") + err := tsm.Run() + assert.NoError(t, err) + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + + // check the callbacks + assert.IsType(t, bacnetip.NewPDU(nil), tsm.GetBeforeSendPdu()) + assert.IsType(t, bacnetip.NewPDU(nil), tsm.GetAfterSendPdu()) + + // make sure pdu was sent + assert.Same(t, pdu, tsm.GetSent()) + + // check the transaction log + require.Equal(t, 1, len(tsm.GetTransactionLog())) + assert.Contains(t, tsm.GetTransactionLog()[0], "PDU") + }) + t.Run("test_state_machine_receive", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // Make a pdu object + pdu := TPDU{} + + // make a send transition from start to success, run the machine + tsm.GetStartState().Receive(pdu, nil).Success("") + err := tsm.Run() + assert.NoError(t, err) + + // check for still running + assert.True(t, tsm.IsRunning()) + + // tell the machine it is receiving the pdu + err = tsm.Receive(bacnetip.NewArgs(pdu), bacnetip.NoKWArgs) + require.NoError(t, err) + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + + // check the callbacks + assert.IsType(t, TPDU{}, tsm.GetBeforeReceivePdu()) + assert.IsType(t, TPDU{}, tsm.GetAfterReceivePdu()) + + // check the transaction log + require.Equal(t, 1, len(tsm.GetTransactionLog())) + assert.Contains(t, tsm.GetTransactionLog()[0], pdu.String()) + }) + t.Run("test_state_machine_unexpected", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // Make a pdu object + goodPdu := TPDU{a: 1} + _ = goodPdu + badPdu := TPDU{b: 2} + + // make a send transition from start to success, run the machine + tsm.GetStartState().Receive(TPDU{}, map[string]any{"a": 1}).Success("") + err := tsm.Run() + assert.NoError(t, err) + + // check for still running + assert.True(t, tsm.IsRunning()) + + // give the machine a bad pdu + err = tsm.Receive(bacnetip.NewArgs(badPdu), bacnetip.NoKWArgs) + require.NoError(t, err) + + // check for fail + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsFailState()) + assert.Same(t, tsm.GetCurrentState(), tsm.GetUnexpectedReceiveState()) + + // check the callbacks + assert.Equal(t, badPdu, tsm.GetUnexpectedReceivePDU()) + + // check the transaction log + require.Equal(t, 1, len(tsm.GetTransactionLog())) + assert.Contains(t, tsm.GetTransactionLog()[0], badPdu.String()) + }) + t.Run("test_state_machine_call", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // simpleHook + called := false + _called := func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error { + called = args[0].(bool) + return nil + } + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // make a send transition from start to success, run the machine + tsm.GetStartState().Call(_called, bacnetip.NewArgs(true), bacnetip.NoKWArgs).Success("") + err := tsm.Run() + assert.NoError(t, err) + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.IsSuccessState()) + + // check for the call + assert.True(t, called) + }) + t.Run("test_state_machine_call_exception", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // simpleHook + called := false + _called := func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error { + called = args[0].(bool) + return tests.AssertionError{Message: "error"} + } + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // make a send transition from start to success, run the machine + tsm.GetStartState().Call(_called, bacnetip.NewArgs(true), bacnetip.NoKWArgs) + err := tsm.Run() + assert.NoError(t, err) + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.IsFailState()) + + // check for the call + assert.True(t, called) + }) + t.Run("test_state_machine_loop_01", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // Make a pdu object + firstPdu := TPDU{a: 1} + t.Log(firstPdu) + secondPdu := TPDU{a: 2} + t.Log(secondPdu) + + // after sending the first pdu, wait for the second + s0 := tsm.GetStartState() + s1 := s0.Send(firstPdu, nil) + s2 := s1.Receive(TPDU{}, map[string]any{"a": 2}) + s2.Success("") + + // run the machine + err := tsm.Run() + assert.NoError(t, err) + + // check for still running and waiting + assert.True(t, tsm.IsRunning()) + assert.Same(t, s1, tsm.GetCurrentState()) + + // give the machine the second pdu + err = tsm.Receive(bacnetip.NewArgs(secondPdu), bacnetip.NoKWArgs) + require.NoError(t, err) + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + t.Log("success") + + // check the callbacks + assert.Equal(t, firstPdu, tsm.GetBeforeSendPdu()) + assert.Equal(t, firstPdu, tsm.GetAfterSendPdu()) + assert.Equal(t, secondPdu, tsm.GetBeforeReceivePdu()) + assert.Equal(t, secondPdu, tsm.GetAfterReceivePdu()) + t.Log("callbacks passed") + + // check the transaction log + assert.Contains(t, tsm.GetTransactionLog()[0], firstPdu.String()) + assert.Contains(t, tsm.GetTransactionLog()[1], secondPdu.String()) + }) + t.Run("test_state_machine_loop_02", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // Make a pdu object + firstPdu := TPDU{a: 1} + t.Log(firstPdu) + secondPdu := TPDU{a: 2} + t.Log(secondPdu) + + // when the first pdu is received, send the second + s0 := tsm.GetStartState() + s1 := s0.Receive(TPDU{}, map[string]any{"a": 1}) + s2 := s1.Send(secondPdu, nil) + s2.Success("") + + // run the machine + err := tsm.Run() + assert.NoError(t, err) + + // check for still running and waiting + assert.True(t, tsm.IsRunning()) + + // give the machine the first pdu + err = tsm.Receive(bacnetip.NewArgs(firstPdu), bacnetip.NoKWArgs) + require.NoError(t, err) + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + t.Log("success") + + // check the callbacks + assert.Equal(t, firstPdu, tsm.GetBeforeReceivePdu()) + assert.Equal(t, firstPdu, tsm.GetAfterReceivePdu()) + assert.Equal(t, secondPdu, tsm.GetBeforeSendPdu()) + assert.Equal(t, secondPdu, tsm.GetAfterSendPdu()) + t.Log("callbacks passed") + + // check the transaction log + assert.Contains(t, tsm.GetTransactionLog()[0], firstPdu.String()) + assert.Contains(t, tsm.GetTransactionLog()[1], secondPdu.String()) + }) +} + +func TestStateMachineTimeout1(t *testing.T) { + tests.LockGlobalTimeMachine(t) + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + + // make a timeout transition from start to success + tsm.GetStartState().Timeout(1*time.Second, nil).Success("") + + tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.Log("time machine reset") + + err := tsm.Run() + require.NoError(t, err) + + tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{}) + t.Log("time machine finished") + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) +} + +func TestStateMachineTimeout2(t *testing.T) { + t.Skip("not ready yet") // TODO: figure out why it is failing + tests.LockGlobalTimeMachine(t) + testingLogger := testutils.ProduceTestingLogger(t) + + // make some pdus + firstPdu := TPDU{a: 1} + t.Log(firstPdu) + secondPdu := TPDU{a: 2} + t.Log(secondPdu) + + // create a state machine + tsm := tests.NewTrappedStateMachine(testingLogger) + s0 := tsm.GetStartState() + + // send something, wait, send something, wait, success + s1 := s0.Send(firstPdu, nil) + s2 := s1.Timeout(1*time.Millisecond, nil) + s3 := s2.Send(secondPdu, nil) + s4 := s3.Timeout(1*time.Millisecond, nil).Success("") + _ = s4 + + tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.Log("time machine reset") + + err := tsm.Run() + require.NoError(t, err) + + tests.RunTimeMachine(testingLogger, 60*time.Millisecond, time.Time{}) + t.Log("time machine finished") + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + + // check the transaction log + assert.Len(t, tsm.GetTransactionLog(), 2) + assert.Contains(t, tsm.GetTransactionLog()[0], firstPdu.String()) + assert.Contains(t, tsm.GetTransactionLog()[1], secondPdu.String()) +} + +func TestStateMachineGroup(t *testing.T) { + t.Run("test_state_machine_group_success", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine group + smg := tests.NewStateMachineGroup(testingLogger) + + // create a trapped state machine, start state is success + tsm := tests.NewTrappedStateMachine(testingLogger) + tsm.GetStartState().Success("") + + // add it to the group + smg.Append(tsm) + + tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.Log("time machine reset") + + // tell the group to run + err := smg.Run() + require.NoError(t, err) + + tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{}) + t.Log("time machine finished") + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsSuccessState()) + assert.False(t, smg.IsRunning()) + assert.True(t, smg.IsSuccessState()) + }) + t.Run("test_state_machine_group_success", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine group + smg := tests.NewStateMachineGroup(testingLogger) + + // create a trapped state machine, start state is success + tsm := tests.NewTrappedStateMachine(testingLogger) + tsm.GetStartState().Fail("") + + // add it to the group + smg.Append(tsm) + + tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.Log("time machine reset") + + // tell the group to run + err := smg.Run() + require.NoError(t, err) + + tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{}) + t.Log("time machine finished") + + // check for success + assert.False(t, tsm.IsRunning()) + assert.True(t, tsm.GetCurrentState().IsFailState()) + assert.False(t, smg.IsRunning()) + assert.True(t, smg.IsFailState()) + }) +} + +func TestStateMachineEvents(t *testing.T) { + t.Run("test_state_machine_event_01", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine group + smg := tests.NewStateMachineGroup(testingLogger) + + // create a trapped state machine, start state is success + tsm1 := tests.NewTrappedStateMachine(testingLogger) + tsm1.GetStartState().SetEvent("e").Success("") + smg.Append(tsm1) + + // create another trapped state machine, waiting for the event + tsm2 := tests.NewTrappedStateMachine(testingLogger) + tsm2.GetStartState().WaitEvent("e", nil).Success("") + smg.Append(tsm2) + + tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.Log("time machine reset") + + // tell the group to run + err := smg.Run() + require.NoError(t, err) + + tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{}) + t.Log("time machine finished") + + // check for success + assert.True(t, tsm1.GetCurrentState().IsSuccessState()) + assert.True(t, tsm2.GetCurrentState().IsSuccessState()) + assert.False(t, smg.IsRunning()) + assert.True(t, smg.IsSuccessState()) + }) + t.Run("test_state_machine_event_02", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + + // create a state machine group + smg := tests.NewStateMachineGroup(testingLogger) + + // create a trapped state machine, waiting for an event + tsm1 := tests.NewTrappedStateMachine(testingLogger) + tsm1.GetStartState().WaitEvent("e", nil).Success("") + smg.Append(tsm1) + + // create another trapped state machine, start state is success + tsm2 := tests.NewTrappedStateMachine(testingLogger) + tsm2.GetStartState().SetEvent("e").Success("") + smg.Append(tsm2) + + tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.Log("time machine reset") + + // tell the group to run + err := smg.Run() + require.NoError(t, err) + + tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{}) + t.Log("time machine finished") + + // check for success + assert.True(t, tsm1.GetCurrentState().IsSuccessState()) + assert.True(t, tsm2.GetCurrentState().IsSuccessState()) + assert.False(t, smg.IsRunning()) + assert.True(t, smg.IsSuccessState()) + }) +} diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go new file mode 100644 index 00000000000..062bd37bd16 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go @@ -0,0 +1,306 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_utilities + +import ( + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type TimeMachineSuite struct { + suite.Suite + + // flag to make sure the function was called + sampleTaskFunctionCalled []time.Time + + log zerolog.Logger +} + +func (suite *TimeMachineSuite) SetupSuite() { + suite.log = testutils.ProduceTestingLogger(suite.T()) +} + +func (suite *TimeMachineSuite) SetupTest() { + tests.LockGlobalTimeMachine(suite.T()) + tests.NewGlobalTimeMachine(suite.log) // TODO: this is really stupid because of concurrency... +} + +func (suite *TimeMachineSuite) TearDownTest() { + tests.ClearGlobalTimeMachine(suite.log) +} + +type SampleOneShotTask struct { + *bacnetip.OneShotTask + + processTaskCalled []time.Time + + log zerolog.Logger +} + +func NewSampleOneShotTask(localLog zerolog.Logger) *SampleOneShotTask { + s := &SampleOneShotTask{ + log: localLog, + } + s.OneShotTask = bacnetip.NewOneShotTask(s, nil) + return s +} + +func (s *SampleOneShotTask) ProcessTask() error { + s.log.Debug().Time("current_time", tests.GlobalTimeMachineCurrentTime()).Msg("processing task") + + // add the current time + s.processTaskCalled = append(s.processTaskCalled, tests.GlobalTimeMachineCurrentTime()) + return nil +} + +func (suite *TimeMachineSuite) SampleTaskFunction() func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error { + return func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error { + currentTime := tests.GlobalTimeMachineCurrentTime() + suite.log.Debug().Stringer("args", args).Stringer("kwArgs", kwArgs).Time("current_time", currentTime).Msg("sample_task_function") + + suite.sampleTaskFunctionCalled = append(suite.sampleTaskFunctionCalled, currentTime) + return nil + } +} + +type SampleRecurringTask struct { + *bacnetip.RecurringTask + + processTaskCalled []time.Time + + log zerolog.Logger +} + +func NewSampleRecurringTask(localLog zerolog.Logger) *SampleRecurringTask { + s := &SampleRecurringTask{ + log: localLog, + } + interval := 1 * time.Nanosecond + s.RecurringTask = bacnetip.NewRecurringTask(localLog, s, &interval, nil) + return s +} + +func (s *SampleRecurringTask) ProcessTask() error { + s.log.Debug().Time("current_time", tests.GlobalTimeMachineCurrentTime()).Msg("processing task") + + // add the current time + s.processTaskCalled = append(s.processTaskCalled, tests.GlobalTimeMachineCurrentTime()) + return nil +} + +func (suite *TimeMachineSuite) TestTimeMachineExists() { + assert.True(suite.T(), tests.IsGlobalTimeMachineSet()) +} + +func (suite *TimeMachineSuite) TestEmptyRun() { + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + + // let it run + tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{}) + + // 60 seconds have passed + suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime)) +} + +func (suite *TimeMachineSuite) TestOneShotImmediate1() { + // create a function task + ft := NewSampleOneShotTask(suite.log) + + // Reset time machine + tests.ResetTimeMachine(tests.StartTime) + var startTime time.Time + ft.InstallTask(bacnetip.InstallTaskOptions{When: &startTime}) + tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Contains(ft.processTaskCalled, startTime) + suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime)) +} + +func (suite *TimeMachineSuite) TestOneShotImmediate2() { + // create a function task + ft := NewSampleOneShotTask(suite.log) + + // run the functions sometime later + t1, err := time.Parse("2006-01-02", "2000-06-06") + suite.Require().NoError(err) + suite.T().Log(t1) + + // reset the time machine to midnight, install the task, let it run + startTime, err := time.Parse("2006-01-02", "2000-01-01") + suite.Require().NoError(err) + tests.ResetTimeMachine(startTime) + ft.InstallTask(bacnetip.InstallTaskOptions{When: &t1}) + tests.RunTimeMachine(suite.log, 0, startTime) + + // function called, 60 seconds passed + suite.Contains(ft.processTaskCalled, t1) +} + +func (suite *TimeMachineSuite) TestFunctionTaskImmediate() { + // create a function task + ft := bacnetip.FunctionTask(suite.SampleTaskFunction(), bacnetip.NoArgs, bacnetip.NoKWArgs) + suite.sampleTaskFunctionCalled = nil + + // reset the time machine to midnight, install the task, let it run + tests.ResetTimeMachine(tests.StartTime) + var now time.Time + ft.InstallTask(bacnetip.InstallTaskOptions{When: &now}) + tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Contains(suite.sampleTaskFunctionCalled, time.Time{}) + suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime)) +} + +func (suite *TimeMachineSuite) TestFunctionTaskDelay() { + sampleDelay := 10 * time.Second + + // create a function task + ft := bacnetip.FunctionTask(suite.SampleTaskFunction(), bacnetip.NoArgs, bacnetip.NoKWArgs) + suite.sampleTaskFunctionCalled = nil + + // reset the time machine to midnight, install the task, let it run + tests.ResetTimeMachine(tests.StartTime) + var now time.Time + when := now.Add(sampleDelay) + ft.InstallTask(bacnetip.InstallTaskOptions{When: &when}) + tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Contains(suite.sampleTaskFunctionCalled, when) + suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime)) +} + +func (suite *TimeMachineSuite) TestRecurringTask1() { + // create a function task + ft := NewSampleRecurringTask(suite.log) + + // reset the time machine to midnight, install the task, let it run + now := tests.StartTime + tests.ResetTimeMachine(now) + interval := 1 * time.Second + ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &interval}) + tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Equal(now.Add(1*time.Second), ft.processTaskCalled[0]) + suite.Equal(now.Add(2*time.Second), ft.processTaskCalled[1]) + suite.Equal(now.Add(3*time.Second), ft.processTaskCalled[2]) + suite.Equal(now.Add(4*time.Second), ft.processTaskCalled[3]) + suite.Equal(now.Add(5*time.Second), ft.processTaskCalled[4]) + suite.Equal(now.Add(6*time.Second), ft.processTaskCalled[5]) + suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(now), float64(100*time.Millisecond)) +} + +func (suite *TimeMachineSuite) TestRecurringTask2() { + // create a function task + ft1 := NewSampleRecurringTask(suite.log) + ft2 := NewSampleRecurringTask(suite.log) + + // reset the time machine to midnight, install the task, let it run + tests.ResetTimeMachine(tests.StartTime) + ft1Interval := 1000 * time.Millisecond + ft1.InstallTask(bacnetip.InstallTaskOptions{Interval: &ft1Interval}) + ft2Interval := 1500 * time.Millisecond + ft2.InstallTask(bacnetip.InstallTaskOptions{Interval: &ft2Interval}) + tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Equal(tests.StartTime.Add(1*time.Second), ft1.processTaskCalled[0]) + suite.Equal(tests.StartTime.Add(2*time.Second), ft1.processTaskCalled[1]) + suite.Equal(tests.StartTime.Add(3*time.Second), ft1.processTaskCalled[2]) + suite.Equal(tests.StartTime.Add(4*time.Second), ft1.processTaskCalled[3]) + suite.Equal(tests.StartTime.Add(1500*time.Millisecond), ft2.processTaskCalled[0]) + suite.Equal(tests.StartTime.Add(3000*time.Millisecond), ft2.processTaskCalled[1]) + suite.Equal(tests.StartTime.Add(4500*time.Millisecond), ft2.processTaskCalled[2]) + suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime), float64(100*time.Millisecond)) +} + +func (suite *TimeMachineSuite) TestRecurringTask3() { + // create a function task + ft := NewSampleRecurringTask(suite.log) + + // reset the time machine to midnight, install the task, let it run + tests.ResetTimeMachine(tests.StartTime) + ftInterval := 1000 * time.Millisecond + ftOffset := 100 * time.Millisecond + ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &ftInterval, Offset: &ftOffset}) + tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Equal(tests.StartTime.Add(100*time.Millisecond), ft.processTaskCalled[0]) + suite.Equal(tests.StartTime.Add(1100*time.Millisecond), ft.processTaskCalled[1]) + suite.Equal(tests.StartTime.Add(2100*time.Millisecond), ft.processTaskCalled[2]) + suite.Equal(tests.StartTime.Add(3100*time.Millisecond), ft.processTaskCalled[3]) + suite.Equal(tests.StartTime.Add(4100*time.Millisecond), ft.processTaskCalled[4]) + suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime), float64(100*time.Millisecond)) +} + +func (suite *TimeMachineSuite) TestRecurringTask4() { + // create a function task + ft := NewSampleRecurringTask(suite.log) + + // reset the time machine to midnight, install the task, let it run + tests.ResetTimeMachine(tests.StartTime) + ftInterval := 1000 * time.Millisecond + ftOffset := -100 * time.Millisecond + ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &ftInterval, Offset: &ftOffset}) + tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{}) + + // function called, 60 seconds passed + suite.Equal(tests.StartTime.Add(900*time.Millisecond), ft.processTaskCalled[0]) + suite.Equal(tests.StartTime.Add(1900*time.Millisecond), ft.processTaskCalled[1]) + suite.Equal(tests.StartTime.Add(2900*time.Millisecond), ft.processTaskCalled[2]) + suite.Equal(tests.StartTime.Add(3900*time.Millisecond), ft.processTaskCalled[3]) + suite.Equal(tests.StartTime.Add(4900*time.Millisecond), ft.processTaskCalled[4]) + suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime), float64(100*time.Millisecond)) +} + +func (suite *TimeMachineSuite) TestRecurringTask5() { + // create a function task + ft := NewSampleRecurringTask(suite.log) + + // reset the time machine, install the task, let it run + now, err := time.Parse("2006-01-02", "2000-01-01") + suite.Require().NoError(err) + tests.ResetTimeMachine(now) + ftInterval := 86400 * time.Second + ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &ftInterval}) + stopTime, err := time.Parse("2006-01-02", "2000-02-01") + suite.Require().NoError(err) + tests.RunTimeMachine(suite.log, 0, stopTime) + + // function called every day + suite.Equal(32, len(ft.processTaskCalled)) +} + +func TestTimeMachine(t *testing.T) { + suite.Run(t, new(TimeMachineSuite)) +} diff --git a/plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go b/plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go new file mode 100644 index 00000000000..4835a2315c4 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go @@ -0,0 +1,475 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_vlan + +import ( + "fmt" + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type TIPNetwork struct { + *tests.StateMachineGroup + + vlan *bacnetip.IPNetwork + + t *testing.T + + log zerolog.Logger +} + +func NewTIPNetwork(t *testing.T, nodeCount int, addressPattern string, promiscuous bool, spoofing bool) *TIPNetwork { + localLog := testutils.ProduceTestingLogger(t) + tn := &TIPNetwork{ + t: t, + log: localLog, + } + tn.StateMachineGroup = tests.NewStateMachineGroup(localLog) + + // make a little LAN + tn.vlan = bacnetip.NewIPNetwork(localLog) + + for i := range nodeCount { + nodeAddress, err := bacnetip.NewAddress(localLog, fmt.Sprintf(addressPattern, i+1)) + require.NoError(t, err) + node, err := bacnetip.NewIPNode(localLog, nodeAddress, tn.vlan, bacnetip.WithNodePromiscuous(promiscuous), bacnetip.WithNodeSpoofing(spoofing)) + require.NoError(t, err) + + // bind a client state machine to the ndoe + csm, err := tests.NewClientStateMachine(localLog) + require.NoError(t, err) + + err = bacnetip.Bind(localLog, csm, node) + require.NoError(t, err) + + // add it to this group + tn.Append(csm) + } + + return tn +} + +func (t *TIPNetwork) Run(timeLimit time.Duration) error { + if timeLimit == 0 { + timeLimit = 60 * time.Second + } + t.log.Debug().Dur("time_limit", timeLimit).Msg("run") + + tests.NewGlobalTimeMachine(t.log) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.log.Trace().Msg("time machine reset") + + // run the group + if err := t.StateMachineGroup.Run(); err != nil { + return err + } + + // run it some time + tests.RunTimeMachine(t.log, timeLimit, time.Time{}) + t.log.Trace().Msg("time machine finished") + + // check for success + success, failed := t.CheckForSuccess() + if !success { + return errors.New("not all succeeded") + } + if failed { + return errors.New("some failed") + } + return nil +} + +func TestIPVLAN(t *testing.T) { + t.Run("test_idle", func(t *testing.T) { // Test that a very quiet network can exist. This is not a network test so much as a state machine group test + tests.LockGlobalTimeMachine(t) + + // two element network + tnet := NewTIPNetwork(t, 2, "192.168.1.%d/24", false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2 := stateMachines[0], stateMachines[1] + + // set the start states of both machines to success + tnode1.GetStartState().Success("") + tnode2.GetStartState().Success("") + + // run the group + err := tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_send_receive", func(t *testing.T) { // Test that a node can send a message to another node. + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // two element network + tnet := NewTIPNetwork(t, 2, "192.168.2.%d/24", false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2 := stateMachines[0], stateMachines[1] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, "192.168.2.1:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, "192.168.2.2:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, mode 2 gets it + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_broadcast", func(t *testing.T) { // Test that a node can send out a 'local broadcast' message which will be received by every other node. + t.Skip("not ready yet") // TODO: figure out why it is failing + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // three element network + tnet := NewTIPNetwork(t, 3, "192.168.3.%d/24", false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, "192.168.3.1:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, "192.168.3.2:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, node 2 and 3 each get it + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_spoof_fail", func(t *testing.T) { // Test verifying that a node cannot send out packets with a source address other than its own, see also test_spoof_pass(). + t.Skip("not ready yet") // TODO: figure out why it is failing + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // one element network + tnet := NewTIPNetwork(t, 1, "192.168.4.%d/24", false, false) + + stateMachines := tnet.GetStateMachines() + tnode1 := stateMachines[0] + + // make an unicast PDU with the wrong source + src, err := bacnetip.NewAddress(testingLogger, "192.168.4.1:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, "192.168.4.2:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, node 2 and 3 each get it + tnode1.GetStartState().Send(pdu, nil).Success("") + + // run the group + err = tnet.Run(0) + assert.Error(t, err) + }) + t.Run("test_spoof_pass", func(t *testing.T) { // Test allowing a node to send out packets with a source address other than its own, see also test_spoof_fail(). + t.Skip("not ready yet") // TODO: figure out why it is failing + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // one element network + tnet := NewTIPNetwork(t, 1, "192.168.5.%d/24", false, true) + + stateMachines := tnet.GetStateMachines() + tnode1 := stateMachines[0] + + // make an unicast PDU with the wrong source + src, err := bacnetip.NewAddress(testingLogger, "192.168.5.1:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, "192.168.5.2:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, but gets it back as if it was from node 3 + tnode1.GetStartState(). + Send(pdu, nil). + Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }). + Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_promiscuous_pass", func(t *testing.T) { // Test 'promiscuous mode' of a node which allows it to receive every packet sent on the network. This is like the network is a hub, or the node is connected to a 'monitor' port on a managed switch. + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // three element network + tnet := NewTIPNetwork(t, 3, "192.168.6.%d/24", true, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, "192.168.6.1:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, "192.168.6.2:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, node 2 and 3 each get it + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduDestination": dest, + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_promiscuous_fail", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // three element network + tnet := NewTIPNetwork(t, 3, "192.168.7.%d/24", true, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, "192.168.7.1:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, "192.168.7.2:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu to node 2, node 3 waits and gets nothing + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // if node 3 receives anything it will trigger unexpected receive and fail + tnode3.GetStartState().Timeout(500*time.Millisecond, nil).Success("") + + // run the group + err = tnet.Run(0) + assert.Error(t, err) + }) +} + +type RouterSuite struct { + suite.Suite + + smg *tests.StateMachineGroup + + log zerolog.Logger +} + +func (suite *RouterSuite) SetupSuite() { + t := suite.T() + suite.log = testutils.ProduceTestingLogger(t) +} + +func (suite *RouterSuite) SetupTest() { + t := suite.T() + t.Skip("not ready yet") // TODO: figure out why it is failing + // create a state machine group that has all nodes on all networks + suite.smg = tests.NewStateMachineGroup(suite.log) + + // make some networks + vlan10 := bacnetip.NewIPNetwork(suite.log) + vlan20 := bacnetip.NewIPNetwork(suite.log) + + // make a router and add the networks + trouter := bacnetip.NewIPRouter(suite.log) + vlan10Addr, err := bacnetip.NewAddress(suite.log, "192.168.10.1/24") + suite.NoError(err) + vlan20Addr, err := bacnetip.NewAddress(suite.log, "192.168.20.1/24") + suite.NoError(err) + trouter.AddNetwork(vlan10Addr, vlan10) + trouter.AddNetwork(vlan20Addr, vlan20) + + for pattern, lan := range map[string]*bacnetip.IPNetwork{ + "192.168.10.%d/24": vlan10, + "192.168.20.%d/24": vlan20, + } { + for i := range 2 { + nodeAddress, err := bacnetip.NewAddress(suite.log, fmt.Sprintf(pattern, i+2)) + suite.NoError(err) + node, err := bacnetip.NewIPNode(suite.log, nodeAddress, lan) + suite.NoError(err) + + // bind a client state machine to the node + csm, err := tests.NewClientStateMachine(suite.log) + suite.NoError(err) + err = bacnetip.Bind(suite.log, csm, node) + suite.NoError(err) + + // add it to the group + suite.smg.Append(csm) + } + } +} + +func (suite *RouterSuite) TearDownTest() { + t := suite.T() + t.Skip("not ready yet") // TODO: figure out why it is failing + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + suite.T().Log("time machine reset") + + // run the group + err := suite.smg.Run() + suite.NoError(err) + + // run it for some time + tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{}) + suite.T().Log("time machine finished") + + // check for success + success, failed := suite.smg.CheckForSuccess() + suite.True(success) + _ = failed +} + +func (suite *RouterSuite) TestIdle() { + t := suite.T() + tests.LockGlobalTimeMachine(t) + + // all success + for _, csm := range suite.smg.GetStateMachines() { + csm.GetStartState().Success("") + } +} + +func (suite *RouterSuite) TestSendReceive() { // Test that a node can send a message to another node. + t := suite.T() + t.Skip("not ready yet") // TODO: figure out why it is failing + tests.LockGlobalTimeMachine(t) + + stateMachines := suite.smg.GetStateMachines() + csm_10_2, csm_10_3, csm_20_2, csm_20_3 := stateMachines[0], stateMachines[1], stateMachines[2], stateMachines[3] + + // make a PDU from network 10 node 1 to network 20 node 2 + src, err := bacnetip.NewAddress(suite.log, "192.168.10.2:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(suite.log, "192.168.20.3:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, mode 2 gets it + csm_10_2.GetStartState().Send(pdu, nil).Success("") + csm_20_3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // other nodes get nothing + csm_10_3.GetStartState().Timeout(1*time.Second, nil).Success("") + csm_20_2.GetStartState().Timeout(1*time.Second, nil).Success("") +} + +func (suite *RouterSuite) TestLocalBroadcast() { // Test that a node can send a message to all of the other nodes on the same network. + t := suite.T() + t.Skip("not ready yet") // TODO: figure out why it is failing + tests.LockGlobalTimeMachine(t) + + stateMachines := suite.smg.GetStateMachines() + csm_10_2, csm_10_3, csm_20_2, csm_20_3 := stateMachines[0], stateMachines[1], stateMachines[2], stateMachines[3] + + // make a PDU from network 10 node 1 to network 20 node 2 + src, err := bacnetip.NewAddress(suite.log, "192.168.10.2:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(suite.log, "192.168.10.255:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 10-2 sends the pdu, node 10-3 gets pdu, nodes 20-2 and 20-3 dont + csm_10_2.GetStartState().Send(pdu, nil).Success("") + csm_10_3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + csm_20_3.GetStartState().Timeout(1*time.Second, nil).Success("") + csm_20_2.GetStartState().Timeout(1*time.Second, nil).Success("") +} + +func (suite *RouterSuite) TestRemoteBroadcast() { // Test that a node can send a message to all of the other nodes on a different network. + t := suite.T() + t.Skip("not ready yet") // TODO: figure out why it is failing + tests.LockGlobalTimeMachine(t) + + stateMachines := suite.smg.GetStateMachines() + csm_10_2, csm_10_3, csm_20_2, csm_20_3 := stateMachines[0], stateMachines[1], stateMachines[2], stateMachines[3] + + // make a PDU from network 10 node 1 to network 20 node 2 + src, err := bacnetip.NewAddress(suite.log, "192.168.10.2:47808") + require.NoError(t, err) + dest, err := bacnetip.NewAddress(suite.log, "192.168.20.255:47808") + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 10-2 sends the pdu, node 10-3 gets pdu, nodes 20-2 and 20-3 dont + csm_10_2.GetStartState().Send(pdu, nil).Success("") + csm_10_3.GetStartState().Timeout(1*time.Second, nil).Success("") + csm_20_2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + csm_20_3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") +} + +func TestRouter(t *testing.T) { + suite.Run(t, new(RouterSuite)) +} diff --git a/plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go b/plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go new file mode 100644 index 00000000000..bf3102f9234 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package test_vlan + +import ( + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + "github.com/apache/plc4x/plc4go/internal/bacnetip/tests" + "github.com/apache/plc4x/plc4go/spi/testutils" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TNetwork struct { + *tests.StateMachineGroup + + vlan *bacnetip.Network + + t *testing.T + + log zerolog.Logger +} + +func NewTNetwork(t *testing.T, nodeCount int, promiscuous bool, spoofing bool) *TNetwork { + localLog := testutils.ProduceTestingLogger(t) + tn := &TNetwork{ + t: t, + log: localLog, + } + tn.StateMachineGroup = tests.NewStateMachineGroup(localLog) + + broadcastAddress, err := bacnetip.NewAddress(localLog, 0) + require.NoError(t, err) + // make a little LAN + tn.vlan = bacnetip.NewNetwork(localLog, bacnetip.WithNetworkBroadcastAddress(broadcastAddress)) + + for i := range nodeCount { + nodeAddress, err := bacnetip.NewAddress(localLog, i+1) + require.NoError(t, err) + node, err := bacnetip.NewNode(localLog, nodeAddress, tn.vlan, bacnetip.WithNodePromiscuous(promiscuous), bacnetip.WithNodeSpoofing(spoofing)) + require.NoError(t, err) + + // bind a client state machine to the ndoe + csm, err := tests.NewClientStateMachine(localLog) + require.NoError(t, err) + + err = bacnetip.Bind(localLog, csm, node) + require.NoError(t, err) + + // add it to this group + tn.Append(csm) + } + + return tn +} + +func (t *TNetwork) Run(timeLimit time.Duration) error { + if timeLimit == 0 { + timeLimit = 60 * time.Second + } + t.log.Debug().Dur("time_limit", timeLimit).Msg("run") + + tests.NewGlobalTimeMachine(t.log) // TODO: this is really stupid because of concurrency... + // reset the time machine + tests.ResetTimeMachine(tests.StartTime) + t.log.Trace().Msg("time machine reset") + + // run the group + if err := t.StateMachineGroup.Run(); err != nil { + return err + } + + // run it some time + tests.RunTimeMachine(t.log, timeLimit, time.Time{}) + t.log.Trace().Msg("time machine finished") + + // check for success + success, failed := t.CheckForSuccess() + if !success { + return errors.New("not all succeeded") + } + if failed { + return errors.New("some failed") + } + return nil +} + +func TestVLAN(t *testing.T) { + t.Run("test_idle", func(t *testing.T) { // Test that a very quiet network can exist. This is not a network test so much as a state machine group test + tests.LockGlobalTimeMachine(t) + + // two element network + tnet := NewTNetwork(t, 2, false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2 := stateMachines[0], stateMachines[1] + + // set the start states of both machines to success + tnode1.GetStartState().Success("") + tnode2.GetStartState().Success("") + + // run the group + err := tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_send_receive", func(t *testing.T) { // Test that a node can send a message to another node. + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // two element network + tnet := NewTNetwork(t, 2, false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2 := stateMachines[0], stateMachines[1] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 2) + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, mode 2 gets it + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_broadcast", func(t *testing.T) { // Test that a node can send out a 'local broadcast' message which will be received by every other node. + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // three element network + tnet := NewTNetwork(t, 3, false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 0) + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, node 2 and 3 each get it + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_spoof_fail", func(t *testing.T) { // Test verifying that a node cannot send out packets with a source address other than its own, see also test_spoof_pass(). + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // one element network + tnet := NewTNetwork(t, 1, false, false) + + stateMachines := tnet.GetStateMachines() + tnode1 := stateMachines[0] + + // make an unicast PDU with the wrong source + src, err := bacnetip.NewAddress(testingLogger, 2) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 3) + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, node 2 and 3 each get it + tnode1.GetStartState().Send(pdu, nil).Success("") + + // run the group + err = tnet.Run(0) + assert.Error(t, err) + }) + t.Run("test_spoof_pass", func(t *testing.T) { // Test allowing a node to send out packets with a source address other than its own, see also test_spoof_fail(). + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // one element network + tnet := NewTNetwork(t, 1, false, true) + + stateMachines := tnet.GetStateMachines() + tnode1 := stateMachines[0] + + // make an unicast PDU with the wrong source + src, err := bacnetip.NewAddress(testingLogger, 3) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, but gets it back as if it was from node 3 + tnode1.GetStartState(). + Send(pdu, nil). + Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }). + Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_promiscuous_pass", func(t *testing.T) { // Test 'promiscuous mode' of a node which allows it to receive every packet sent on the network. This is like the network is a hub, or the node is connected to a 'monitor' port on a managed switch. + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // three element network + tnet := NewTNetwork(t, 3, true, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 2) + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu, node 2 and 3 each get it + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduDestination": dest, + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) + t.Run("test_promiscuous_fail", func(t *testing.T) { + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // three element network + tnet := NewTNetwork(t, 3, true, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 2) + require.NoError(t, err) + pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(pdu) + + // node 1 sends the pdu to node 2, node 3 waits and gets nothing + tnode1.GetStartState().Send(pdu, nil).Success("") + tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduSource": src, + }).Success("") + + // if node 3 receives anything it will trigger unexpected receive and fail + tnode3.GetStartState().Timeout(500*time.Millisecond, nil).Success("") + + // run the group + err = tnet.Run(0) + assert.Error(t, err) + }) +} + +func TestVLANEvents(t *testing.T) { + t.Run("test_send_receive", func(t *testing.T) { // Test that a node can send a message to another node and use events to continue with the messages. + testingLogger := testutils.ProduceTestingLogger(t) + tests.LockGlobalTimeMachine(t) + + // two element network + tnet := NewTNetwork(t, 2, false, false) + + stateMachines := tnet.GetStateMachines() + tnode1, tnode2 := stateMachines[0], stateMachines[1] + + // make a PDU from node 1 to node 2 + src, err := bacnetip.NewAddress(testingLogger, 1) + require.NoError(t, err) + dest, err := bacnetip.NewAddress(testingLogger, 2) + require.NoError(t, err) + + deadPDU := bacnetip.NewPDU(tests.NewDummyMessage(0xde, 0xad), bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(deadPDU) + + // make a PDU from node 1 to node 2 + beefPDU := bacnetip.NewPDU(tests.NewDummyMessage(0xbe, 0xef), bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest)) + t.Log(beefPDU) + + // node 1 sends dead_pdu, waits for event, sends beef_pdu + tnode1.GetStartState(). + Send(deadPDU, nil).WaitEvent("e", nil). + Send(beefPDU, nil).Success("") + + // node 2 receives dead_pdu, sets event, waits for beef_pdu + tnode2.GetStartState(). + Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduData": tests.NewDummyMessage(0xde, 0xad), + }).SetEvent("e"). + Receive(bacnetip.NewPDU(nil), map[string]any{ + "pduData": tests.NewDummyMessage(0xbe, 0xef), + }).Success("") + + // run the group + err = tnet.Run(0) + assert.NoError(t, err) + }) +} diff --git a/plc4go/internal/bacnetip/tests/time_machine.go b/plc4go/internal/bacnetip/tests/time_machine.go new file mode 100644 index 00000000000..7b4fde8ff3f --- /dev/null +++ b/plc4go/internal/bacnetip/tests/time_machine.go @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tests + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + + "github.com/rs/zerolog" +) + +var globalTimeMachine *TimeMachine +var globalTimeMachineMutex sync.Mutex + +func IsGlobalTimeMachineSet() bool { + return globalTimeMachine != nil +} + +func NewGlobalTimeMachine(localLog zerolog.Logger) { + if globalTimeMachine != nil { + localLog.Warn().Msg("global time machine set, overwriting") + } + globalTimeMachine = NewTimeMachine(localLog) +} + +func ClearGlobalTimeMachine(localLog zerolog.Logger) { + if globalTimeMachine == nil { + localLog.Warn().Msg("global time machine not set") + } + globalTimeMachine = nil +} + +func LockGlobalTimeMachine(t *testing.T) { + globalTimeMachineMutex.Lock() + t.Cleanup(globalTimeMachineMutex.Unlock) +} + +type TimeMachine struct { + bacnetip.TaskManager + + currentTime time.Time + timeLimit time.Time + startTime time.Time + + log zerolog.Logger +} + +func NewTimeMachine(localLog zerolog.Logger) *TimeMachine { + t := &TimeMachine{ + log: localLog, + } + t.TaskManager = bacnetip.NewTaskManager(localLog) + bacnetip.OverwriteTaskManager(t) + return t +} + +func (t *TimeMachine) GetTime() time.Time { + t.log.Debug().Time("currentTime", t.currentTime).Msg("GetTime") + return t.currentTime +} + +func (t *TimeMachine) InstallTask(task bacnetip.TaskRequirements) { + t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("InstallTask") + t.TaskManager.InstallTask(task) +} + +func (t *TimeMachine) SuspendTask(task bacnetip.TaskRequirements) { + t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("SuspendTask") + t.TaskManager.SuspendTask(task) +} + +func (t *TimeMachine) ResumeTask(task bacnetip.TaskRequirements) { + t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("ResumeTask") + t.TaskManager.ResumeTask(task) +} + +func (t *TimeMachine) MoreToDo() bool { + t.log.Debug().Time("currentTime", t.currentTime).Msg("MoreToDo") + if len(bacnetip.DeferredFunctions) > 0 { + t.log.Trace().Msg("deferredFunctions") + return true + } + + t.log.Debug().Time("timeLimit", t.timeLimit).Msg("timeLimit") + if t.log.Debug().Enabled() { + stringers := make([]fmt.Stringer, len(t.GetTasks())) + for i, task := range t.GetTasks() { //TODO: check if there is something more efficient + stringers[i] = task + } + t.log.Debug().Stringers("tasks", stringers).Msg("tasks") + } + + if !t.timeLimit.IsZero() && t.currentTime.After(t.timeLimit) { + t.log.Trace().Msg("time limit reached") + return false + } + + if len(t.GetTasks()) == 0 { + t.log.Trace().Msg("no more tasks") + return false + } + + task := t.GetTasks()[0] + when := task.GetTaskTime() + if when.After(t.timeLimit) { + t.log.Debug().Msg("New tasks exceeded time limit") + return false + } + t.log.Debug().Stringer("task", task).Msg("task") + return true +} + +func (t *TimeMachine) GetNextTask() (bacnetip.TaskRequirements, *time.Duration) { + t.log.Debug().Time("currentTime", t.currentTime).Msg("GetNextTask") + t.log.Debug().Time("timeLimit", t.timeLimit).Msg("timeLimit") + if t.log.Debug().Enabled() { + stringers := make([]fmt.Stringer, len(t.GetTasks())) + for i, task := range t.GetTasks() { //TODO: check if there is something more efficient + stringers[i] = task + } + t.log.Debug().Stringers("tasks", stringers).Msg("tasks") + } + + var task bacnetip.TaskRequirements + var delta *time.Duration + + if !t.timeLimit.IsZero() && t.currentTime.After(t.timeLimit) { + t.log.Trace().Msg("time limit reached") + } else if len(t.GetTasks()) == 0 { + t.log.Trace().Msg("no more tasks") + } else { + task = t.GetTasks()[0] + if taskTime := task.GetTaskTime(); taskTime != nil && task.GetTaskTime().After(t.timeLimit) { + t.currentTime = *taskTime + } else { + // pull it off the list + task = t.PopTask() + t.log.Debug().Stringer("task", task).Msg("when, task") + + // mark that it is no longer scheduled + task.SetIsScheduled(false) + + // advance the time + if taskTime := task.GetTaskTime(); taskTime != nil { + t.currentTime = *taskTime + } + + // do not wait, time has moved + var newDelta time.Duration + delta = &newDelta + } + } + return task, delta +} + +func (t *TimeMachine) ProcessTask(task bacnetip.TaskRequirements) { + t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("ProcessTask") + t.TaskManager.ProcessTask(task) +} + +// ResetTimeMachine This function is called to reset the clock before running a set of tests. +func ResetTimeMachine(startTime time.Time) { + if globalTimeMachine == nil { + panic("no time machine") + } + + globalTimeMachine.ClearTasks() + globalTimeMachine.currentTime = startTime + globalTimeMachine.timeLimit = time.Time{} +} + +// RunTimeMachine This function is called after a set of tasks have been installed +// +// and they should Run. The machine will stop when the stop time has been +// reached (maybe the middle of some tests) and can be called again to +// continue running. +func RunTimeMachine(localLog zerolog.Logger, duration time.Duration, stopTime time.Time) { + if globalTimeMachine == nil { + panic("no time machine") + } + localLog.Debug().Dur("duration", duration).Time("stopTime", stopTime).Msg("RunTimeMachine") + + /* TODO: we don't have a proper tristate, maybe we change currentTime to a pointer + if !globalTimeMachine.currentTime.IsZero() { + panic("Reset the time machine before running") + }*/ + + if duration != 0 { + globalTimeMachine.timeLimit = globalTimeMachine.currentTime.Add(duration) + } else if !stopTime.IsZero() { + globalTimeMachine.timeLimit = stopTime + } else { + panic("duration or stopTime is required") + } + + if len(bacnetip.DeferredFunctions) > 0 { + localLog.Debug().Msg("deferredFunctions") + } + + for { + bacnetip.RunOnce(localLog) + localLog.Trace().Msg("ran once") + if !globalTimeMachine.MoreToDo() { + localLog.Trace().Msg("no more to do") + break + } + } + + globalTimeMachine.currentTime = globalTimeMachine.timeLimit +} + +// GlobalTimeMachineCurrentTime Return the current time from the time machine. +func GlobalTimeMachineCurrentTime() time.Time { + return globalTimeMachine.currentTime +} diff --git a/plc4go/internal/bacnetip/tests/trapped_classes.go b/plc4go/internal/bacnetip/tests/trapped_classes.go new file mode 100644 index 00000000000..37f7df1981a --- /dev/null +++ b/plc4go/internal/bacnetip/tests/trapped_classes.go @@ -0,0 +1,569 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tests + +import ( + "fmt" + + "github.com/apache/plc4x/plc4go/internal/bacnetip" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type TrapperRequirements interface { + BeforeSend(pdu bacnetip.PDU) + AfterSend(pdu bacnetip.PDU) + BeforeReceive(pdu bacnetip.PDU) + AfterReceive(pdu bacnetip.PDU) + UnexpectedReceive(pdu bacnetip.PDU) +} + +// Trapper This class provides a set of utility functions that keeps the latest copy of the pdu parameter in the +// before_send(), after_send(), before_receive(), after_receive() and unexpected_receive() calls. +type Trapper struct { + TrapperRequirements + + beforeSendPdu bacnetip.PDU + afterSendPdu bacnetip.PDU + beforeReceivePdu bacnetip.PDU + afterReceivePdu bacnetip.PDU + unexpectedReceivePdu bacnetip.PDU + + log zerolog.Logger +} + +func NewTrapper(localLog zerolog.Logger, requirements TrapperRequirements) *Trapper { + trapper := &Trapper{ + TrapperRequirements: requirements, + log: localLog, + } + // reset to initialize + trapper.reset() + return trapper +} + +func (t *Trapper) reset() { + t.log.Trace().Msg("Reset") + // flush the copies + t.beforeSendPdu = nil + t.afterSendPdu = nil + t.beforeReceivePdu = nil + t.afterReceivePdu = nil + t.unexpectedReceivePdu = nil +} + +// BeforeSend is Called before each PDU about to be sent. +func (t *Trapper) BeforeSend(pdu bacnetip.PDU) { + t.log.Debug().Stringer("pdu", pdu).Msg("BeforeSend") + //keep a copy + t.beforeSendPdu = pdu + + // continue + t.TrapperRequirements.BeforeSend(pdu) +} + +func (t *Trapper) GetBeforeSendPdu() bacnetip.PDU { + return t.beforeSendPdu +} + +// AfterSend is Called after each PDU sent. +func (t *Trapper) AfterSend(pdu bacnetip.PDU) { + t.log.Debug().Stringer("pdu", pdu).Msg("AfterSend") + //keep a copy + t.afterSendPdu = pdu + + // continue + t.TrapperRequirements.AfterSend(pdu) +} + +func (t *Trapper) GetAfterSendPdu() bacnetip.PDU { + return t.afterSendPdu +} + +// BeforeReceive is Called with each PDU received before matching. +func (t *Trapper) BeforeReceive(pdu bacnetip.PDU) { + t.log.Debug().Stringer("pdu", pdu).Msg("BeforeReceive") + //keep a copy + t.beforeReceivePdu = pdu + + // continue + t.TrapperRequirements.BeforeReceive(pdu) +} + +func (t *Trapper) GetBeforeReceivePdu() bacnetip.PDU { + return t.beforeReceivePdu +} + +// AfterReceive is Called with PDU received after match. +func (t *Trapper) AfterReceive(pdu bacnetip.PDU) { + t.log.Debug().Stringer("pdu", pdu).Msg("AfterReceive") + //keep a copy + t.afterReceivePdu = pdu + + // continue + t.TrapperRequirements.AfterReceive(pdu) +} + +func (t *Trapper) GetAfterReceivePdu() bacnetip.PDU { + return t.afterReceivePdu +} + +// UnexpectedReceive is Called with PDU that did not match. Unless this is trapped by the state, the default behaviour is to fail. +func (t *Trapper) UnexpectedReceive(pdu bacnetip.PDU) { + t.log.Debug().Stringer("pdu", pdu).Msg("UnexpectedReceive") + //keep a copy + t.unexpectedReceivePdu = pdu + + // continue + t.TrapperRequirements.UnexpectedReceive(pdu) +} + +func (t *Trapper) GetUnexpectedReceivePDU() bacnetip.PDU { + return t.unexpectedReceivePdu +} + +// TrappedState This class is a simple wrapper around the state class that keeps the latest copy of the pdu parameter in +// the BeforeSend(), AfterSend(), BeforeReceive(), AfterReceive() and UnexpectedReceive() calls. +type TrappedState struct { + *Trapper + State +} + +func NewTrappedState(state State, trapper *Trapper) *TrappedState { + t := &TrappedState{ + State: state, + Trapper: trapper, + } + return t +} + +func (t *TrappedState) Equals(other State) bool { + if t.State.Equals(other) { //TODO: we always want to match the inner + return true + } + if otherTs, ok := other.(*TrappedState); ok { + return t.State.Equals(otherTs.State) + } + return false +} + +func (t *TrappedState) String() string { + return fmt.Sprintf("TrappedState(%v)", t.State) +} + +func (t *TrappedState) BeforeSend(pdu bacnetip.PDU) { + t.Trapper.BeforeSend(pdu) +} + +func (t *TrappedState) AfterSend(pdu bacnetip.PDU) { + t.Trapper.AfterSend(pdu) +} + +func (t *TrappedState) BeforeReceive(pdu bacnetip.PDU) { + t.Trapper.BeforeReceive(pdu) +} + +func (t *TrappedState) AfterReceive(pdu bacnetip.PDU) { + t.Trapper.AfterReceive(pdu) +} + +func (t *TrappedState) UnexpectedReceive(pdu bacnetip.PDU) { + t.Trapper.UnexpectedReceive(pdu) +} + +func (t *TrappedState) getInterceptor() StateInterceptor { + return t +} + +// TrappedStateMachine This class is a simple wrapper around the stateMachine class that keeps the +// +// latest copy of the pdu parameter in the BeforeSend(), AfterSend(), BeforeReceive(), AfterReceive() and UnexpectedReceive() calls. +// +// It also provides a send() function, so when the machine runs it doesn't +// throw an exception. +type TrappedStateMachine struct { + *Trapper + StateMachine + + sent bacnetip.PDU + + log zerolog.Logger +} + +func NewTrappedStateMachine(localLog zerolog.Logger) *TrappedStateMachine { + t := &TrappedStateMachine{ + log: localLog, + } + var init func() + t.StateMachine, init = NewStateMachine(localLog, t, WithStateMachineStateInterceptor(t), WithStateMachineStateDecorator(t.DecorateState)) + t.Trapper = NewTrapper(localLog, t.StateMachine) + init() // bit later so everything is set up + return t +} + +func (t *TrappedStateMachine) GetSent() bacnetip.PDU { + return t.sent +} + +func (t *TrappedStateMachine) BeforeSend(pdu bacnetip.PDU) { + t.StateMachine.BeforeSend(pdu) +} + +func (t *TrappedStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send") + // keep a copy + t.sent = args.Get0PDU() + return nil +} + +func (t *TrappedStateMachine) AfterSend(pdu bacnetip.PDU) { + t.StateMachine.AfterSend(pdu) +} + +func (t *TrappedStateMachine) BeforeReceive(pdu bacnetip.PDU) { + t.StateMachine.BeforeReceive(pdu) +} + +func (t *TrappedStateMachine) AfterReceive(pdu bacnetip.PDU) { + t.StateMachine.AfterReceive(pdu) +} + +func (t *TrappedStateMachine) UnexpectedReceive(pdu bacnetip.PDU) { + t.StateMachine.UnexpectedReceive(pdu) +} + +func (t *TrappedStateMachine) DecorateState(state State) State { + return NewTrappedState(state, t.Trapper) +} + +type TrappedClientRequirements interface { + Request(bacnetip.Args, bacnetip.KWArgs) error +} + +// TrappedClient An instance of this class sits at the top of a stack. +type TrappedClient struct { + TrappedClientRequirements + *bacnetip.Client + + requestSent bacnetip.PDU + confirmationReceived bacnetip.PDU + + log zerolog.Logger +} + +func NewTrappedClient(localLog zerolog.Logger, requirements TrappedClientRequirements) (*TrappedClient, error) { + t := &TrappedClient{ + TrappedClientRequirements: requirements, + log: localLog, + } + var err error + t.Client, err = bacnetip.NewClient(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "error building client") + } + return t, nil +} + +func (t *TrappedClient) GetRequestSent() bacnetip.PDU { + return t.requestSent +} + +func (t *TrappedClient) GetConfirmationReceived() bacnetip.PDU { + return t.confirmationReceived +} + +func (t *TrappedClient) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Request") + // a reference for checking + t.requestSent = args.Get0PDU() + + // Call sub + return t.TrappedClientRequirements.Request(args, kwargs) +} + +func (t *TrappedClient) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation") + // a reference for checking + t.confirmationReceived = args.Get0PDU() + return nil +} + +type TrappedServerRequirements interface { + Response(bacnetip.Args, bacnetip.KWArgs) error +} + +// TrappedServer An instance of this class sits at the bottom of a stack. +type TrappedServer struct { + TrappedServerRequirements + *bacnetip.Server + + indicationReceived bacnetip.PDU + responseSent bacnetip.PDU + + log zerolog.Logger +} + +func NewTrappedServer(localLog zerolog.Logger, requirements TrappedServerRequirements) (*TrappedServer, error) { + t := &TrappedServer{ + TrappedServerRequirements: requirements, + log: localLog, + } + var err error + t.Server, err = bacnetip.NewServer(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "error building server") + } + return t, nil +} + +func (t *TrappedServer) GetIndicationReceived() bacnetip.PDU { + return t.indicationReceived +} + +func (t *TrappedServer) GetResponseSent() bacnetip.PDU { + return t.responseSent +} + +func (t *TrappedServer) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + // a reference for checking + t.indicationReceived = args.Get0PDU() + + return nil +} + +func (t *TrappedServer) Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Response") + // a reference for checking + t.responseSent = args.Get0PDU() + + // Call sub + return t.TrappedServerRequirements.Response(args, kwargs) +} + +type TrappedServerStateMachine struct { + *TrappedServer + *TrappedStateMachine + + log zerolog.Logger +} + +func NewTrappedServerStateMachine(localLog zerolog.Logger) (*TrappedServerStateMachine, error) { + t := &TrappedServerStateMachine{log: localLog} + var err error + t.TrappedServer, err = NewTrappedServer(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "error building trapped server") + } + t.TrappedStateMachine = NewTrappedStateMachine(localLog) + return t, nil +} + +func (t *TrappedServerStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send") + return t.Response(args, kwargs) +} + +func (t *TrappedServerStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + return t.Receive(args, kwargs) +} + +type TrappedServiceAccessPointRequirements interface { + SapRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error + SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error + SapResponse(args bacnetip.Args, kwargs bacnetip.KWArgs) error + SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +// TrappedServiceAccessPoint Note that while this class inherits from ServiceAccessPoint, it doesn't +// +// provide any stubbed behavior for SapIndication() or SapConfirmation(), +// so if these functions are called it will still raise panic. +// +// To provide these functions, write a ServiceAccessPoint derived class and +// stuff it in the inheritance sequence: +// +// struct Snort{ +// ServiceAccessPoint +// } +// func SapIndication(pdu): +// ...do something... +// func SapConfirmation(pdu): +// ...do something... +// +// struct TrappedSnort(TrappedServiceAccessPoint, Snort) +// +// The Snort functions will be called after the PDU is trapped. +type TrappedServiceAccessPoint struct { + TrappedServiceAccessPointRequirements + *bacnetip.ServiceAccessPoint + + sapRequestSent bacnetip.PDU + sapIndicationReceived bacnetip.PDU + sapResponseSent bacnetip.PDU + sapConfirmationReceived bacnetip.PDU + + log zerolog.Logger +} + +func NewTrappedServiceAccessPoint(localLog zerolog.Logger, requirements TrappedServiceAccessPointRequirements) (*TrappedServiceAccessPoint, error) { + t := &TrappedServiceAccessPoint{ + TrappedServiceAccessPointRequirements: requirements, + log: localLog, + } + var err error + t.ServiceAccessPoint, err = bacnetip.NewServiceAccessPoint(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "error building service access point") + } + return t, nil +} + +func (s *TrappedServiceAccessPoint) GetSapRequestSent() bacnetip.PDU { + return s.sapRequestSent +} +func (s *TrappedServiceAccessPoint) GetSapIndicationReceived() bacnetip.PDU { + return s.sapIndicationReceived +} +func (s *TrappedServiceAccessPoint) GetSapResponseSent() bacnetip.PDU { + return s.sapResponseSent +} +func (s *TrappedServiceAccessPoint) GetSapConfirmationReceived() bacnetip.PDU { + return s.sapConfirmationReceived +} + +func (s *TrappedServiceAccessPoint) SapRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapRequest") + s.sapRequestSent = args.Get0PDU() + return s.TrappedServiceAccessPointRequirements.SapRequest(args, kwargs) +} + +func (s *TrappedServiceAccessPoint) SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapIndication") + s.sapIndicationReceived = args.Get0PDU() + return s.TrappedServiceAccessPointRequirements.SapIndication(args, kwargs) +} + +func (s *TrappedServiceAccessPoint) SapResponse(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapResponse") + s.sapResponseSent = args.Get0PDU() + return s.TrappedServiceAccessPointRequirements.SapResponse(args, kwargs) +} + +func (s *TrappedServiceAccessPoint) SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapConfirmation") + s.sapConfirmationReceived = args.Get0PDU() + return s.TrappedServiceAccessPointRequirements.SapConfirmation(args, kwargs) +} + +type TrappedApplicationServiceElementRequirements interface { + Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error + Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error + Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error + Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error +} + +// TrappedApplicationServiceElement Note that while this class inherits from ApplicationServiceElement, it +// +// doesn't provide any stubbed behavior for indication() or confirmation(), +// so if these functions are called it will still raise NotImplementedError. +// +// To provide these functions, write a ServiceAccessPoint derived class and +// stuff it in the inheritance sequence: +// +// class Snort(ApplicationServiceElement): +// def indication(self, pdu): +// ...do something... +// def confirmation(self, pdu): +// ...do something... +// +// class TrappedSnort(TrappedApplicationServiceElement, Snort): pass +// +// The Snort functions will be called after the PDU is trapped. +type TrappedApplicationServiceElement struct { + TrappedApplicationServiceElementRequirements + *bacnetip.ApplicationServiceElement + + requestSent bacnetip.PDU + indicationReceived bacnetip.PDU + responseSent bacnetip.PDU + confirmationReceived bacnetip.PDU + + log zerolog.Logger +} + +func NewTrappedApplicationServiceElement(localLog zerolog.Logger, requirements TrappedApplicationServiceElementRequirements) (*TrappedApplicationServiceElement, error) { + t := &TrappedApplicationServiceElement{ + TrappedApplicationServiceElementRequirements: requirements, + log: localLog, + } + var err error + t.ApplicationServiceElement, err = bacnetip.NewApplicationServiceElement(localLog, t) + if err != nil { + return nil, errors.Wrap(err, "error building application service element") + } + return t, nil +} + +func (s *TrappedApplicationServiceElement) GetRequestSent() bacnetip.PDU { + return s.requestSent +} + +func (s *TrappedApplicationServiceElement) GetIndicationReceived() bacnetip.PDU { + return s.indicationReceived +} + +func (s *TrappedApplicationServiceElement) GetResponseSent() bacnetip.PDU { + return s.responseSent +} + +func (s *TrappedApplicationServiceElement) GetConfirmationReceived() bacnetip.PDU { + return s.confirmationReceived +} + +func (s *TrappedApplicationServiceElement) String() string { + return fmt.Sprintf("TrappedApplicationServiceElement(TBD...)") // TODO: fill some info here +} + +func (s *TrappedApplicationServiceElement) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Request") + s.requestSent = args.Get0PDU() + return s.TrappedApplicationServiceElementRequirements.Request(args, kwargs) +} + +func (s *TrappedApplicationServiceElement) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication") + s.indicationReceived = args.Get0PDU() + return s.TrappedApplicationServiceElementRequirements.Indication(args, kwargs) +} + +func (s *TrappedApplicationServiceElement) Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Response") + s.responseSent = args.Get0PDU() + return s.TrappedApplicationServiceElementRequirements.Response(args, kwargs) +} + +func (s *TrappedApplicationServiceElement) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error { + s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation") + s.confirmationReceived = args.Get0PDU() + return s.TrappedApplicationServiceElementRequirements.Confirmation(args, kwargs) +} diff --git a/plc4go/internal/bacnetip/tests/util.go b/plc4go/internal/bacnetip/tests/util.go new file mode 100644 index 00000000000..82888091282 --- /dev/null +++ b/plc4go/internal/bacnetip/tests/util.go @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tests + +import ( + "context" + "encoding/hex" + "fmt" + "time" + + "github.com/apache/plc4x/plc4go/spi/utils" +) + +var StartTime = time.Time{}.Add(1 * time.Hour) + +type DummyMessage struct { + Data []byte +} + +func NewDummyMessage(data ...byte) *DummyMessage { + return &DummyMessage{Data: data} +} + +func (d DummyMessage) String() string { + return hex.EncodeToString(d.Data) +} + +func (d DummyMessage) Serialize() ([]byte, error) { + return d.Data, nil +} + +func (d DummyMessage) SerializeWithWriteBuffer(ctx context.Context, writeBuffer utils.WriteBuffer) error { + return writeBuffer.WriteSerializable(ctx, d) +} + +func (d DummyMessage) GetLengthInBytes(_ context.Context) uint16 { + return uint16(len(d.Data)) +} + +func (d DummyMessage) GetLengthInBits(_ context.Context) uint16 { + return uint16(len(d.Data)) +} + +type AssertionError struct { + Message string +} + +func (a AssertionError) Error() string { + return fmt.Sprintf("AssertionError: %s", a.Message) +}