cosmos 3.8.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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")