github.com/simpleiot/simpleiot@v0.18.3/client/rule_test.go (about)

     1  package client_test
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/nats-io/nats.go"
     9  	"github.com/simpleiot/simpleiot/client"
    10  	"github.com/simpleiot/simpleiot/data"
    11  	"github.com/simpleiot/simpleiot/server"
    12  )
    13  
    14  type ruleTestServer struct {
    15  	t         *testing.T
    16  	root      data.NodeEdge
    17  	nc        *nats.Conn
    18  	stop      func()
    19  	vin       client.Variable
    20  	vin2      client.Variable
    21  	vout      client.Variable
    22  	r         client.Rule
    23  	c         client.Condition
    24  	c2        client.Condition
    25  	a         client.Action
    26  	a2        client.ActionInactive
    27  	voutGet   func() client.Variable
    28  	voutStop  func()
    29  	lastvout  float64
    30  	lastCheck string
    31  }
    32  
    33  func (rts *ruleTestServer) checkVout(expected float64, msg string, pointKey string) {
    34  	rts.lastCheck = msg
    35  	if rts.lastvout == expected {
    36  		// vout is not changing, so delay here to make sure the rule
    37  		// has time to run before we check the result
    38  		time.Sleep(time.Millisecond * 75)
    39  	}
    40  
    41  	start := time.Now()
    42  	for {
    43  		if rts.voutGet().Value[pointKey] == expected {
    44  			rts.lastvout = expected
    45  			// all is well
    46  			break
    47  		}
    48  		if time.Since(start) > time.Second {
    49  			rts.t.Fatalf("vout failed, expected: %v, test: %v", expected, msg)
    50  		}
    51  		<-time.After(time.Millisecond * 10)
    52  	}
    53  }
    54  
    55  func (rts *ruleTestServer) sendPoint(id string, point data.Point) {
    56  	point.Origin = "test"
    57  	err := client.SendNodePoint(rts.nc, id, point, true)
    58  
    59  	if err != nil {
    60  		rts.t.Errorf("Error sending point: %v, last check: %v", err, rts.lastCheck)
    61  	}
    62  }
    63  
    64  func setupRuleTest(t *testing.T, numConditions int) (ruleTestServer, error) {
    65  
    66  	var r ruleTestServer
    67  	var err error
    68  
    69  	r.t = t
    70  
    71  	r.nc, r.root, r.stop, err = server.TestServer()
    72  
    73  	if err != nil {
    74  		return r, fmt.Errorf("Error starting test server: %w", err)
    75  	}
    76  	// send test nodes to Db
    77  	r.vin = client.Variable{
    78  		ID:          "ID-varin",
    79  		Parent:      r.root.ID,
    80  		Description: "var in",
    81  	}
    82  
    83  	err = client.SendNodeType(r.nc, r.vin, "test")
    84  	if err != nil {
    85  		return r, fmt.Errorf("Error sending vin node: %w", err)
    86  	}
    87  
    88  	r.vout = client.Variable{
    89  		ID:          "ID-varout",
    90  		Parent:      r.root.ID,
    91  		Description: "var out",
    92  	}
    93  
    94  	err = client.SendNodeType(r.nc, r.vout, "test")
    95  	if err != nil {
    96  		return r, fmt.Errorf("Error sending vout node: %w", err)
    97  	}
    98  
    99  	r.r = client.Rule{
   100  		ID:          "ID-rule",
   101  		Parent:      r.root.ID,
   102  		Description: "test rule",
   103  		Disabled:    false,
   104  	}
   105  
   106  	err = client.SendNodeType(r.nc, r.r, "test")
   107  	if err != nil {
   108  		return r, fmt.Errorf("Error sending r node: %w", err)
   109  	}
   110  
   111  	r.c = client.Condition{
   112  		ID:            "ID-condition",
   113  		Parent:        r.r.ID,
   114  		Description:   "cond vin high",
   115  		ConditionType: data.PointValuePointValue,
   116  		PointType:     data.PointTypeValue,
   117  		ValueType:     data.PointValueOnOff,
   118  		NodeID:        r.vin.ID,
   119  		Operator:      data.PointValueEqual,
   120  		Value:         1,
   121  	}
   122  
   123  	err = client.SendNodeType(r.nc, r.c, "test")
   124  	if err != nil {
   125  		return r, fmt.Errorf("Error sending c node: %w", err)
   126  	}
   127  
   128  	if numConditions > 1 {
   129  		// send test nodes to Db
   130  		r.vin2 = client.Variable{
   131  			ID:          "ID-varin2",
   132  			Parent:      r.root.ID,
   133  			Description: "var in2",
   134  		}
   135  
   136  		err = client.SendNodeType(r.nc, r.vin2, "test")
   137  		if err != nil {
   138  			return r, fmt.Errorf("Error sending vin2 node: %w", err)
   139  		}
   140  
   141  		r.c2 = client.Condition{
   142  			ID:            "ID-condition2",
   143  			Parent:        r.r.ID,
   144  			Description:   "cond vin2 high",
   145  			ConditionType: data.PointValuePointValue,
   146  			PointType:     data.PointTypeValue,
   147  			ValueType:     data.PointValueOnOff,
   148  			NodeID:        r.vin2.ID,
   149  			Operator:      data.PointValueEqual,
   150  			Value:         1,
   151  		}
   152  
   153  		err = client.SendNodeType(r.nc, r.c2, "test")
   154  		if err != nil {
   155  			return r, fmt.Errorf("Error sending c node: %w", err)
   156  		}
   157  	}
   158  
   159  	r.a = client.Action{
   160  		ID:          "ID-action-active",
   161  		Parent:      r.r.ID,
   162  		Description: "action active",
   163  		Action:      data.PointValueSetValue,
   164  		PointType:   data.PointTypeValue,
   165  		NodeID:      r.vout.ID,
   166  		Value:       1,
   167  	}
   168  
   169  	err = client.SendNodeType(r.nc, r.a, "test")
   170  	if err != nil {
   171  		return r, fmt.Errorf("Error sending a node: %w", err)
   172  	}
   173  
   174  	// FIXME:
   175  	// this delay is required to work around a bug in the manager
   176  	// where it is resetting and does not see the ActionInactive points
   177  	// See https://github.com/simpleiot/simpleiot/issues/630
   178  	// the tools/test-rules.sh script can be used to test a fix for this
   179  	// problem
   180  	time.Sleep(100 * time.Millisecond)
   181  
   182  	r.a2 = client.ActionInactive{
   183  		ID:          "ID-action-inactive",
   184  		Parent:      r.r.ID,
   185  		Description: "action inactive",
   186  		Action:      data.PointValueSetValue,
   187  		PointType:   data.PointTypeValue,
   188  		NodeID:      r.vout.ID,
   189  		Value:       0,
   190  	}
   191  
   192  	err = client.SendNodeType(r.nc, r.a2, "test")
   193  	if err != nil {
   194  		return r, fmt.Errorf("Error sending a2 node: %w", err)
   195  	}
   196  
   197  	// set up a node watcher to watch the output variable
   198  	r.voutGet, r.voutStop, err = client.NodeWatcher[client.Variable](r.nc, r.vout.ID, r.vout.Parent)
   199  
   200  	if err != nil {
   201  		return r, fmt.Errorf("Error setting up watcher: %w", err)
   202  	}
   203  
   204  	// wait for rule to get set up
   205  	time.Sleep(250 * time.Millisecond)
   206  
   207  	return r, nil
   208  }
   209  
   210  // TestRules populates a rule in the system that watches
   211  // a variable and when set, sets another variable. This
   212  // tests out the basic rule logic.
   213  func TestRule(t *testing.T) {
   214  	r, err := setupRuleTest(t, 1)
   215  	if err != nil {
   216  		t.Fatal("Rule test setup failed: ", err)
   217  	}
   218  
   219  	defer r.stop()
   220  	defer r.voutStop()
   221  
   222  	r.checkVout(0, "initial value", "0")
   223  
   224  	// set vin and look for vout to change
   225  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   226  	r.checkVout(1, "look for vout to change after set vin", "0")
   227  
   228  	// clear vin and look for vout to change
   229  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 0})
   230  	r.checkVout(0, "look for vout to clear", "0")
   231  }
   232  
   233  /*
   234  leave everything enabled and toggle vin and watch vout toggle -- same as the TestRules() function. This ensures that your test is setup correctly.
   235  disable rule, set vin and verify vout does not get set. Then clear vin.
   236  - enable rule, and disable condition. set vin and verify vout does not get set. Clear vin.
   237  - enable condition, and disable action. set vin and verify vout does not get set. Clear vin.
   238  - enable action, set vin, then disable rule. verify vout gets cleared.
   239  - enable rule, and verify vout gets set.
   240  - disable condition, and verify vout gets cleared.
   241  - enable condition, and verify vout gets set.
   242  */
   243  func TestRuleDisabled(t *testing.T) {
   244  	r, err := setupRuleTest(t, 1)
   245  	if err != nil {
   246  		t.Fatal("Rule test setup failed: ", err)
   247  	}
   248  
   249  	defer r.stop()
   250  	defer r.voutStop()
   251  
   252  	r.checkVout(0, "check initial state", "0")
   253  
   254  	// everything enabled, set vin and look for vout to change
   255  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   256  	r.checkVout(1, "set vin and look for vout to change", "0")
   257  
   258  	// everything enabled, clear vin and look for vout to change
   259  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 0})
   260  	r.checkVout(0, "clear vin and look for vout to change", "0")
   261  
   262  	// disable rule, set vin and verify vout does not get set. Then clear vin.
   263  	r.sendPoint(r.r.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   264  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   265  	r.checkVout(0, "disable rule, set vin and verify vout does not get set", "0")
   266  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 0})
   267  
   268  	// enable rule, and disable condition. set vin and verify vout does not get set.
   269  	r.sendPoint(r.r.ID, data.Point{Type: data.PointTypeDisabled, Value: 0})
   270  	r.sendPoint(r.c.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   271  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   272  	r.checkVout(0, "disable condition and verify vout does not get set", "0")
   273  
   274  	// enable condition and verify vout gets set again. Clear vin.
   275  	r.sendPoint(r.c.ID, data.Point{Type: data.PointTypeDisabled, Value: 0})
   276  	r.checkVout(1, "re-enable condition and verify vout is set.", "0")
   277  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 0})
   278  
   279  	// enable condition, and disable action. set vin and verify vout does not get set. Clear vin.
   280  	r.sendPoint(r.a.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   281  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   282  	r.checkVout(0, "disable action and verify vout does not get set.", "0")
   283  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 0})
   284  
   285  	// 	enable action, set vin, then disable rule. verify vout gets cleared.
   286  	r.sendPoint(r.a.ID, data.Point{Type: data.PointTypeDisabled, Value: 0})
   287  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   288  	r.checkVout(1, "disable rule, initial state", "0")
   289  	r.sendPoint(r.r.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   290  	r.checkVout(0, "disable rule, vout cleared.", "0")
   291  
   292  	// enable rule, and verify vout gets set.
   293  	r.sendPoint(r.r.ID, data.Point{Type: data.PointTypeDisabled, Value: 0})
   294  	r.checkVout(1, "enable rule, and verify vout gets set.", "0")
   295  
   296  	// disable condition, and verify vout gets cleared.
   297  	r.sendPoint(r.c.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   298  	r.checkVout(0, "disable condition, and verify vout gets cleared.", "0")
   299  
   300  	// enable condition, and verify vout gets set.
   301  	r.sendPoint(r.c.ID, data.Point{Type: data.PointTypeDisabled, Value: 0})
   302  	r.checkVout(1, "enable condition, and verify vout gets set.", "0")
   303  }
   304  
   305  /*
   306  if one condition is active and the 2nd condition is disabled, the rule fires
   307  if both conditions are disabled, the rule is inactive.
   308  */
   309  func TestRuleMultipleConditions(t *testing.T) {
   310  	r, err := setupRuleTest(t, 2)
   311  	if err != nil {
   312  		t.Fatal("Rule test setup failed: ", err)
   313  	}
   314  
   315  	defer r.stop()
   316  	defer r.voutStop()
   317  
   318  	r.checkVout(0, "initial condition", "0")
   319  
   320  	// both conditions enabled
   321  	// if one condition is active and the 2nd condition is inactive, the rule should not fire
   322  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   323  	r.checkVout(0, "1st active, 2nd inactive", "0")
   324  
   325  	// if both conditions are active the rule should fire
   326  	r.sendPoint(r.vin2.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   327  	r.checkVout(1, "both active", "0")
   328  
   329  	// if one condition is disabled, the rule should still fire because
   330  	// the enabled condition is still active
   331  	r.sendPoint(r.c.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   332  	r.checkVout(1, "one condition enabled and action", "0")
   333  
   334  	// if both conditions are active but disabled, the rule is inactive.
   335  	r.sendPoint(r.c.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   336  	r.sendPoint(r.c2.ID, data.Point{Type: data.PointTypeDisabled, Value: 1})
   337  	r.checkVout(0, "both active and disabled", "0")
   338  }
   339  
   340  /*
   341  Test PointKey of Action Node.
   342  */
   343  func TestRuleActionPointKey(t *testing.T) {
   344  	r, err := setupRuleTest(t, 1)
   345  	if err != nil {
   346  		t.Fatal("Rule test setup failed: ", err)
   347  	}
   348  
   349  	// we are setting the an action with key set to "1", so modify the rule
   350  	r.sendPoint(r.a.ID, data.Point{Type: data.PointTypePointKey, Text: "1"})
   351  	r.sendPoint(r.a2.ID, data.Point{Type: data.PointTypePointKey, Text: "1"})
   352  
   353  	defer r.stop()
   354  	defer r.voutStop()
   355  
   356  	r.checkVout(0, "inital value", "1")
   357  
   358  	// check if point is set correctly.
   359  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 1})
   360  	r.checkVout(1, "should be high", "1")
   361  
   362  	// check if point is cleared correctly
   363  	r.sendPoint(r.vin.ID, data.Point{Type: data.PointTypeValue, Value: 0})
   364  
   365  	r.checkVout(0, "should be low", "1")
   366  }