cosmos 3.8.0 → 3.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/autohotkey/tools/packet_viewer.ahk +4 -0
  3. data/cosmos.gemspec +1 -1
  4. data/data/crc.txt +277 -277
  5. data/demo/Gemfile +2 -2
  6. data/demo/config/data/crc.txt +176 -176
  7. data/demo/config/targets/INST/cmd_tlm/_ccsds_cmd.txt +2 -2
  8. data/demo/config/targets/INST/cmd_tlm/inst_cmds.txt +4 -4
  9. data/demo/procedures/example_test.rb +4 -0
  10. data/install/Gemfile +1 -1
  11. data/install/config/data/crc.txt +112 -112
  12. data/lib/cosmos/config/config_parser.rb +35 -1
  13. data/lib/cosmos/core_ext/string.rb +21 -17
  14. data/lib/cosmos/core_ext/time.rb +6 -2
  15. data/lib/cosmos/gui/opengl/gl_viewer.rb +4 -4
  16. data/lib/cosmos/gui/opengl/stl_shape.rb +5 -1
  17. data/lib/cosmos/gui/qt.rb +0 -26
  18. data/lib/cosmos/io/io_multiplexer.rb +27 -45
  19. data/lib/cosmos/packets/packet.rb +64 -24
  20. data/lib/cosmos/packets/packet_config.rb +254 -54
  21. data/lib/cosmos/packets/packet_item.rb +39 -10
  22. data/lib/cosmos/packets/parsers/packet_item_parser.rb +7 -2
  23. data/lib/cosmos/script/commands.rb +5 -0
  24. data/lib/cosmos/script/scripting.rb +5 -5
  25. data/lib/cosmos/script/telemetry.rb +5 -0
  26. data/lib/cosmos/tools/cmd_tlm_server/api.rb +22 -0
  27. data/lib/cosmos/tools/limits_monitor/limits_monitor.rb +38 -10
  28. data/lib/cosmos/tools/packet_viewer/packet_viewer.rb +48 -9
  29. data/lib/cosmos/tools/test_runner/test_runner.rb +76 -14
  30. data/lib/cosmos/tools/tlm_viewer/widgets/linegraph_widget.rb +11 -2
  31. data/lib/cosmos/tools/tlm_viewer/widgets/timegraph_widget.rb +15 -12
  32. data/lib/cosmos/top_level.rb +29 -32
  33. data/lib/cosmos/version.rb +4 -4
  34. data/spec/config/config_parser_spec.rb +8 -15
  35. data/spec/core_ext/socket_spec.rb +2 -2
  36. data/spec/core_ext/string_spec.rb +10 -0
  37. data/spec/core_ext/time_spec.rb +12 -4
  38. data/spec/io/io_multiplexer_spec.rb +11 -3
  39. data/spec/packets/packet_spec.rb +30 -0
  40. data/spec/script/commands_spec.rb +2 -1
  41. data/spec/script/scripting_spec.rb +22 -0
  42. data/spec/script/telemetry_spec.rb +2 -1
  43. data/spec/spec_helper.rb +2 -2
  44. data/spec/tools/cmd_tlm_server/router_thread_spec.rb +2 -2
  45. data/spec/top_level/top_level_spec.rb +4 -2
  46. metadata +5 -5
@@ -103,7 +103,11 @@ module Cosmos
103
103
  @select_keyseq = Qt::KeySequence.new(tr('Ctrl+S'))
104
104
  @select.shortcut = @select_keyseq
105
105
  @select.statusTip = tr('Select Test Suites/Groups/Cases')
106
- @select.connect(SIGNAL('triggered()')) { show_select}
106
+ @select.connect(SIGNAL('triggered()')) { show_select }
107
+
108
+ @file_options = Qt::Action.new(tr('O&ptions'), self)
109
+ @file_options.statusTip = tr('Application Options')
110
+ @file_options.connect(SIGNAL('triggered()')) { file_options() }
107
111
 
108
112
  # Script Actions
109
113
  @test_results_log_message = Qt::Action.new(tr('Log Message to Test Results'), self)
@@ -141,21 +145,23 @@ module Cosmos
141
145
 
142
146
  def initialize_menus
143
147
  # File Menu
144
- @file_menu = menuBar.addMenu(tr('&File'))
145
- @file_menu.addAction(@show_last)
146
- @file_menu.addAction(@select)
147
- @file_menu.addSeparator()
148
- @file_menu.addAction(@exit_action)
148
+ file_menu = menuBar.addMenu(tr('&File'))
149
+ file_menu.addAction(@show_last)
150
+ file_menu.addAction(@select)
151
+ file_menu.addSeparator()
152
+ file_menu.addAction(@file_options)
153
+ file_menu.addSeparator()
154
+ file_menu.addAction(@exit_action)
149
155
 
150
156
  # Script Menu
151
- @script_menu = menuBar.addMenu(tr('&Script'))
152
- @script_menu.addAction(@test_results_log_message)
153
- @script_menu.addAction(@script_log_message)
154
- @script_menu.addAction(@show_call_stack)
155
- @script_menu.addAction(@toggle_debug)
156
- @script_menu.addAction(@script_disconnect)
157
- @script_menu.addSeparator()
158
- @script_menu.addAction(@script_audit)
157
+ script_menu = menuBar.addMenu(tr('&Script'))
158
+ script_menu.addAction(@test_results_log_message)
159
+ script_menu.addAction(@script_log_message)
160
+ script_menu.addAction(@show_call_stack)
161
+ script_menu.addAction(@toggle_debug)
162
+ script_menu.addAction(@script_disconnect)
163
+ script_menu.addSeparator()
164
+ script_menu.addAction(@script_audit)
159
165
 
160
166
  # Help Menu
161
167
  @about_string = "Test Runner provides a framework for developing high " \
@@ -816,6 +822,7 @@ module Cosmos
816
822
  Qt::DialogButtonBox::Cancel)
817
823
  connect(button_box, SIGNAL('rejected()'), box, SLOT('reject()'))
818
824
  connect(button_box, SIGNAL('accepted()')) do
825
+ ScriptRunnerFrame.instance = @script_runner_frame
819
826
  Cosmos.module_eval("class CustomTestSuite < TestSuite; end")
820
827
  tree.topLevelItems do |suite_node|
821
828
  next if suite_node.checkState == Qt::Unchecked
@@ -880,6 +887,7 @@ module Cosmos
880
887
  @test_runner_chooser.test_suites = @@suites
881
888
  @test_runner_chooser.select_suite("CustomTestSuite")
882
889
  end
890
+ ScriptRunnerFrame.instance = nil
883
891
  box.accept
884
892
  end
885
893
  dialog_layout.addWidget(button_box)
@@ -890,6 +898,60 @@ module Cosmos
890
898
  dialog.dispose
891
899
  end
892
900
 
901
+ def file_options
902
+ dialog = Qt::Dialog.new(self)
903
+ dialog.setWindowTitle('Test Runner Options')
904
+ layout = Qt::VBoxLayout.new
905
+
906
+ form = Qt::FormLayout.new
907
+ box = Qt::DoubleSpinBox.new
908
+ box.setRange(0, 60)
909
+ box.setValue(ScriptRunnerFrame.line_delay)
910
+ form.addRow(tr("&Delay between each script line:"), box)
911
+ monitor = Qt::CheckBox.new
912
+ form.addRow(tr("&Monitor limits:"), monitor)
913
+ pause_on_red = Qt::CheckBox.new
914
+ form.addRow(tr("Pause on &red limit:"), pause_on_red)
915
+ if ScriptRunnerFrame.monitor_limits
916
+ monitor.setCheckState(Qt::Checked)
917
+ pause_on_red.setCheckState(Qt::Checked) if ScriptRunnerFrame.pause_on_red
918
+ else
919
+ pause_on_red.setEnabled(false)
920
+ end
921
+ monitor.connect(SIGNAL('stateChanged(int)')) do
922
+ if monitor.isChecked()
923
+ pause_on_red.setEnabled(true)
924
+ else
925
+ pause_on_red.setCheckState(Qt::Unchecked)
926
+ pause_on_red.setEnabled(false)
927
+ end
928
+ end
929
+ layout.addLayout(form)
930
+
931
+ divider = Qt::Frame.new
932
+ divider.setFrameStyle(Qt::Frame::HLine | Qt::Frame::Raised)
933
+ divider.setLineWidth(1)
934
+ layout.addWidget(divider)
935
+
936
+ ok = Qt::PushButton.new('Ok')
937
+ ok.setDefault(true)
938
+ ok.connect(SIGNAL('clicked(bool)')) do
939
+ ScriptRunnerFrame.line_delay = box.value
940
+ ScriptRunnerFrame.monitor_limits = (monitor.checkState == Qt::Checked)
941
+ ScriptRunnerFrame.pause_on_red = (pause_on_red.checkState == Qt::Checked)
942
+ dialog.accept
943
+ end
944
+ cancel = Qt::PushButton.new('Cancel')
945
+ cancel.connect(SIGNAL('clicked(bool)')) { dialog.reject }
946
+ button_layout = Qt::HBoxLayout.new
947
+ button_layout.addWidget(ok)
948
+ button_layout.addWidget(cancel)
949
+ layout.addLayout(button_layout)
950
+ dialog.setLayout(layout)
951
+ dialog.exec
952
+ dialog.dispose
953
+ end
954
+
893
955
  def process_config(filename)
894
956
  ScriptRunnerFrame.instance = @script_runner_frame
895
957
 
@@ -33,10 +33,19 @@ module Cosmos
33
33
  end
34
34
 
35
35
  def value=(data)
36
- @data << data.to_f
36
+ if data.is_a?(Array)
37
+ data2 = data.map(&:to_f)
38
+ data2.reject!{|val| val.nan? or val.infinite?}
39
+ @data.push(data2).flatten!
40
+ else
41
+ data2 = data.to_f
42
+ if !data2.infinite? and !data2.nan?
43
+ @data << data2
44
+ end
45
+ end
37
46
 
38
47
  if @data.length > @num_samples
39
- @data = @data[1..-1]
48
+ @data = @data.last(@num_samples)
40
49
  end
41
50
  if not @data.empty?
42
51
  self.clear_lines
@@ -66,21 +66,24 @@ module Cosmos
66
66
  # Don't regraph old data
67
67
  return if @time[-1] == t_sec
68
68
 
69
- # create time array
70
- @time << t_sec
69
+ data2 = data.to_f
70
+ if data2.infinite? or data2.nan?
71
+ # create time array
72
+ @time << t_sec
71
73
 
72
- # create data array and graph
73
- @data << data.to_f
74
+ # create data array and graph
75
+ @data << data2
74
76
 
75
- # truncate data if necessary
76
- if @data.length > @num_samples
77
- @data = @data[1..-1]
78
- @time = @time[1..-1]
79
- end
77
+ # truncate data if necessary
78
+ if @data.length > @num_samples
79
+ @data = @data[1..-1]
80
+ @time = @time[1..-1]
81
+ end
80
82
 
81
- self.clear_lines
82
- self.add_line('line', @data, @time)
83
- self.graph
83
+ self.clear_lines
84
+ self.add_line('line', @data, @time)
85
+ self.graph
86
+ end
84
87
  end
85
88
 
86
89
  end
@@ -162,11 +162,12 @@ module Cosmos
162
162
  # No Gemfile - so no gem based extensions
163
163
  end
164
164
 
165
+ # Check CORE
165
166
  filename = File.join(::Cosmos::PATH, 'data', name)
166
167
  return filename if File.exist? filename
167
168
 
168
- # Check CORE
169
- filename = Cosmos.path('data', name)
169
+ # Check relative to executing file
170
+ filename = Cosmos.path($0, 'config/data/' + name)
170
171
  return filename if File.exist? filename
171
172
 
172
173
  nil
@@ -304,7 +305,7 @@ module Cosmos
304
305
  if real_lines > 0
305
306
  Logger.error output
306
307
  self.write_unexpected_file(output)
307
- if defined? ::Qt and ::Qt::Application.instance
308
+ if defined? ::Qt and $qApp
308
309
  Qt.execute_in_main_thread(false) do
309
310
  dialog = Qt::Dialog.new do |box|
310
311
  box.setWindowTitle('Unexpected text output')
@@ -528,7 +529,7 @@ module Cosmos
528
529
  Logger.level = Logger::FATAL unless try_gui
529
530
  Logger.fatal "Fatal Exception! Exiting..."
530
531
  Logger.fatal error.formatted
531
- if defined? ExceptionDialog and try_gui and Qt::Application.instance
532
+ if defined? ExceptionDialog and try_gui and $qApp
532
533
  Qt.execute_in_main_thread(true) {||ExceptionDialog.new(nil, error, '', true, false, log_file)}
533
534
  else
534
535
  if $stdout != STDOUT
@@ -618,39 +619,35 @@ module Cosmos
618
619
  # @param filename [String] Name of the file to open in the editor
619
620
  def self.open_in_text_editor(filename)
620
621
  if filename
621
- if Kernel.is_windows?
622
- if File.extname(filename).to_s.downcase == '.csv'
623
- self.run_process("cmd /c \"start wordpad \"#{filename.gsub('/','\\')}\"\"")
624
- else
625
- self.run_process("cmd /c \"start \"\" \"#{filename.gsub('/','\\')}\"\"")
626
- end
627
- elsif Kernel.is_mac?
628
- self.run_process("open -a TextEdit \"#{filename}\"")
622
+ if ENV['COSMOS_TEXT']
623
+ self.run_process("#{ENV['COSMOS_TEXT']} \"#{filename}\"")
629
624
  else
630
- which_gedit = `which gedit 2>&1`.chomp
631
- if which_gedit =~ /Command not found/i or which_gedit =~ /no .* in/i
632
- # No gedit
633
- editor = ENV['EDITOR']
634
- editor = 'vi' unless editor
635
- which_xterm = `which xterm 2>&1`.chomp
636
- if which_xterm =~ /Command not found/i or which_xterm =~ /no .* in/i
637
- # No xterm
638
- which_gnome_terminal = `which gnome-terminal 2>&1`.chomp
639
- if which_gnome_terminal =~ /Command not found/i or which_gnome_terminal =~ /no .* in/i
640
- # No gnome-terminal - Do nothing
641
- else
642
- # Have gnome-terminal
643
- system_call = "gnome-terminal -e #{editor} \"#{filename}\""
644
- end
625
+ if Kernel.is_windows?
626
+ if File.extname(filename).to_s.downcase == '.csv'
627
+ self.run_process("cmd /c \"start wordpad \"#{filename.gsub('/','\\')}\"\"")
645
628
  else
646
- # Have xterm
647
- system_call = "xterm -e #{editor} \"#{filename}\""
629
+ self.run_process("cmd /c \"start \"\" \"#{filename.gsub('/','\\')}\"\"")
648
630
  end
631
+ elsif Kernel.is_mac?
632
+ self.run_process("open -a TextEdit \"#{filename}\"")
649
633
  else
650
- # Have gedit
651
- system_call = "gedit \"#{filename}\""
634
+ which_gedit = `which gedit 2>&1`.chomp
635
+ if which_gedit.to_s.strip == "" or which_gedit =~ /Command not found/i or which_gedit =~ /no .* in/i
636
+ # No gedit
637
+ ['xterm', 'gnome-terminal', 'urxvt', 'rxvt'].each do |terminal|
638
+ which_terminal = `which #{terminal} 2>&1`.chomp
639
+ next if which_terminal.to_s.strip == "" or which_terminal =~ /Command not found/i or which_terminal =~ /no .* in/i
640
+ editor = ENV['VISUAL']
641
+ editor = ENV['EDITOR'] unless editor
642
+ editor = 'vi' unless editor
643
+ self.run_process("#{terminal} -e \"#{editor} '#{filename}'\"")
644
+ break
645
+ end
646
+ else
647
+ # Have gedit
648
+ self.run_process("gedit \"#{filename}\"")
649
+ end
652
650
  end
653
- self.run_process(system_call)
654
651
  end
655
652
  end
656
653
  end
@@ -1,12 +1,12 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- COSMOS_VERSION = '3.8.0'
3
+ COSMOS_VERSION = '3.8.1'
4
4
  module Cosmos
5
5
  module Version
6
6
  MAJOR = '3'
7
7
  MINOR = '8'
8
- PATCH = '0'
9
- BUILD = '2bed164a724153c0accb6392928da087b2120ade'
8
+ PATCH = '1'
9
+ BUILD = 'fa125f2a38e0aa28f6d3000a80ea5167115172d7'
10
10
  end
11
- VERSION = '3.8.0'
11
+ VERSION = '3.8.1'
12
12
  end
@@ -221,11 +221,6 @@ module Cosmos
221
221
  expect(ConfigParser.handle_nil("HI")).to eql "HI"
222
222
  expect(ConfigParser.handle_nil(5.0)).to eql 5.0
223
223
  end
224
-
225
- #it "should complain if it can't convert" do
226
- # expect { ConfigParser.handle_nil(0) }.to raise_error(ArgumentError, "Value is not nil: 0")
227
- # expect { ConfigParser.handle_nil(false) }.to raise_error(ArgumentError, "Value is not nil: false")
228
- #end
229
224
  end
230
225
 
231
226
  describe "self.handle_true_false" do
@@ -245,11 +240,6 @@ module Cosmos
245
240
  expect(ConfigParser.handle_true_false("HI")).to eql "HI"
246
241
  expect(ConfigParser.handle_true_false(5.0)).to eql 5.0
247
242
  end
248
-
249
- #it "should complain if it can't convert" do
250
- # expect { ConfigParser.handle_true_false(0) }.to raise_error(ArgumentError, "Value neither true or false: 0")
251
- # expect { ConfigParser.handle_true_false(nil) }.to raise_error(ArgumentError, "Value neither true or false: ")
252
- #end
253
243
  end
254
244
 
255
245
  describe "self.handle_true_false_nil" do
@@ -281,15 +271,18 @@ module Cosmos
281
271
  expect(ConfigParser.handle_true_false("HI")).to eql "HI"
282
272
  expect(ConfigParser.handle_true_false(5.0)).to eql 5.0
283
273
  end
284
-
285
- #it "should complain if it can't convert" do
286
- # expect { ConfigParser.handle_true_false_nil(0) }.to raise_error(ArgumentError, "Value neither true, false, or nil: 0")
287
- # expect { ConfigParser.handle_true_false_nil(1) }.to raise_error(ArgumentError, "Value neither true, false, or nil: 1")
288
- #end
289
274
  end
290
275
 
291
276
  describe "self.handle_defined_constants" do
292
277
  it "converts string constants to numbers" do
278
+ (1..64).each do |val|
279
+ # Unsigned
280
+ expect(ConfigParser.handle_defined_constants("MIN", :UINT, val)).to eql 0
281
+ expect(ConfigParser.handle_defined_constants("MAX", :UINT, val)).to eql (2**val - 1)
282
+ # Signed
283
+ expect(ConfigParser.handle_defined_constants("MIN", :INT, val)).to eql -((2**val) / 2)
284
+ expect(ConfigParser.handle_defined_constants("MAX", :INT, val)).to eql ((2**val) / 2 - 1)
285
+ end
293
286
  [8,16,32,64].each do |val|
294
287
  # Unsigned
295
288
  expect(ConfigParser.handle_defined_constants("MIN_UINT#{val}")).to eql 0
@@ -25,8 +25,8 @@ describe Socket do
25
25
 
26
26
  describe "lookup_hostname_from_ip" do
27
27
  it "returns the hostname for the ip address" do
28
- ipaddr = Resolv.getaddress "www.ball.com"
29
- expect(Socket.lookup_hostname_from_ip(ipaddr)).to match "ball.com"
28
+ ipaddr = Resolv.getaddress "localhost"
29
+ expect(Socket.lookup_hostname_from_ip(ipaddr)).to match "localhost"
30
30
  end
31
31
  end
32
32
  end
@@ -194,6 +194,16 @@ describe String do
194
194
  it "converts an array" do
195
195
  expect("[0,1,2,3]".convert_to_value).to eql [0,1,2,3]
196
196
  end
197
+
198
+ it "just returns the string if something goes wrong" do
199
+ expect("[.a,2,3]".convert_to_value).to eql "[.a,2,3]"
200
+ end
201
+
202
+ it "doesn't match multiline strings" do
203
+ expect("12345\n12345".convert_to_value).to eql "12345\n12345"
204
+ expect("5.123\n5.123".convert_to_value).to eql "5.123\n5.123"
205
+ expect("[0,1,2,3]\n[0,1,2,3]".convert_to_value).to eql "[0,1,2,3]\n[0,1,2,3]"
206
+ end
197
207
  end
198
208
 
199
209
  describe "hex_to_byte_string" do
@@ -113,12 +113,20 @@ describe Time do
113
113
  end
114
114
 
115
115
  describe "formatted" do
116
- it "formats the Time" do
117
- expect(Time.new(2020,1,2,3,4,5.5).formatted).to eql "2020/01/02 03:04:05.500"
116
+ it "formats the Time with date and fractional seconds" do
117
+ expect(Time.new(2020,1,2,3,4,5.123456).formatted(true, 6)).to eql "2020/01/02 03:04:05.123456"
118
118
  end
119
119
 
120
- it "formats the Time without the date" do
121
- expect(Time.new(2020,1,2,3,4,5.5).formatted(false)).to eql "03:04:05.500"
120
+ it "formats the Time with date and no fractional seconds" do
121
+ expect(Time.new(2020,1,2,3,4,5.123456).formatted(true, 0)).to eql "2020/01/02 03:04:05"
122
+ end
123
+
124
+ it "formats the Time without the date and fractional seconds" do
125
+ expect(Time.new(2020,1,2,3,4,5.123456).formatted(false, 2)).to eql "03:04:05.12"
126
+ end
127
+
128
+ it "formats the Time without the date and no fractional seconds" do
129
+ expect(Time.new(2020,1,2,3,4,5.123456).formatted(false, 0)).to eql "03:04:05"
122
130
  end
123
131
  end
124
132
 
@@ -18,6 +18,15 @@ module Cosmos
18
18
  @io = IoMultiplexer.new
19
19
  end
20
20
 
21
+ describe "stream_operator" do
22
+ it "supports the << operator" do
23
+ @io.add_stream(STDOUT)
24
+ expect($stdout).to receive(:<<).with("TEST").and_return($stdout)
25
+ result = (@io << "TEST")
26
+ expect(result).to eql(@io)
27
+ end
28
+ end
29
+
21
30
  describe "add_stream" do
22
31
  it "adds a single stream" do
23
32
  @io.add_stream(STDOUT)
@@ -64,10 +73,10 @@ module Cosmos
64
73
  describe "write write_nonblock" do
65
74
  it "defers to the stream" do
66
75
  @io.add_stream(STDOUT)
67
- expect($stdout).to receive(:write).with("TEST")
76
+ expect($stdout).to receive(:write).with("TEST").and_return(4)
68
77
  len = @io.write "TEST"
69
78
  expect(len).to eql 4
70
- expect($stdout).to receive(:write_nonblock).with("TEST")
79
+ expect($stdout).to receive(:write_nonblock).with("TEST").and_return(4)
71
80
  len = @io.write_nonblock "TEST"
72
81
  expect(len).to eql 4
73
82
  end
@@ -88,7 +97,6 @@ module Cosmos
88
97
  File.delete("unittest.txt")
89
98
  end
90
99
  end
91
-
92
100
  end
93
101
  end
94
102
 
@@ -370,6 +370,36 @@ module Cosmos
370
370
  expect(@p.read_item(i, :CONVERTED, "\x02")).to eql 1
371
371
  end
372
372
 
373
+ it "prevents the read conversion cache from being corrupted" do
374
+ @p.append_item("item",8,:UINT)
375
+ i = @p.get_item("ITEM")
376
+ i.read_conversion = GenericConversion.new("'A String'")
377
+ i.units = "with units"
378
+ value = @p.read_item(i, :CONVERTED)
379
+ expect(value).to eql 'A String'
380
+ value << 'That got modified'
381
+ value = @p.read_item(i, :WITH_UNITS)
382
+ expect(value).to eql 'A String with units'
383
+ value << 'That got modified'
384
+ expect(@p.read_item(i, :WITH_UNITS)).to eql 'A String with units'
385
+ value = @p.read_item(i, :WITH_UNITS)
386
+ value << ' more things'
387
+ expect(@p.read_item(i, :WITH_UNITS)).to eql 'A String with units'
388
+
389
+ @p.buffer = "\x00"
390
+ i.read_conversion = GenericConversion.new("['A', 'B', 'C']")
391
+ value = @p.read_item(i, :CONVERTED)
392
+ expect(value).to eql ['A', 'B', 'C']
393
+ value << 'D'
394
+ value = @p.read_item(i, :WITH_UNITS)
395
+ expect(value).to eql ['A with units', 'B with units', 'C with units']
396
+ value << 'D'
397
+ expect(@p.read_item(i, :WITH_UNITS)).to eql ['A with units', 'B with units', 'C with units']
398
+ value = @p.read_item(i, :WITH_UNITS)
399
+ value << 'D'
400
+ expect(@p.read_item(i, :WITH_UNITS)).to eql ['A with units', 'B with units', 'C with units']
401
+ end
402
+
373
403
  it "reads the CONVERTED value with states" do
374
404
  @p.append_item("item",8,:UINT)
375
405
  i = @p.get_item("ITEM")