openc3-cosmos-cfdp 1.0.2 → 2.0.0
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.
- checksums.yaml +4 -4
- data/README.md +10 -1
- data/microservices/CFDP/Gemfile +3 -11
- data/microservices/CFDP/Gemfile.dev +35 -0
- data/microservices/CFDP/Gemfile.prod +29 -0
- data/microservices/CFDP/app/controllers/cfdp_controller.rb +8 -0
- data/microservices/CFDP/app/models/cfdp_crc_checksum.rb +3 -1
- data/microservices/CFDP/app/models/cfdp_mib.rb +2 -1
- data/microservices/CFDP/app/models/cfdp_pdu.rb +43 -19
- data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +7 -2
- data/microservices/CFDP/app/models/cfdp_source_transaction.rb +6 -1
- data/microservices/CFDP/app/models/cfdp_transaction.rb +1 -1
- data/microservices/CFDP/app/models/cfdp_user.rb +14 -4
- data/microservices/CFDP/config/application.rb +0 -11
- data/microservices/CFDP/config/environments/test.rb +1 -1
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_file_data.rb +6 -2
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_finished.rb +17 -5
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_metadata.rb +28 -11
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_tlv.rb +5 -2
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_user_ops.rb +16 -9
- data/microservices/CFDP/spec/controllers/cfdp_controller_spec.rb +373 -0
- data/microservices/CFDP/spec/models/cfdp_crc_checksum_spec.rb +134 -0
- data/microservices/CFDP/spec/models/cfdp_mib_spec.rb +389 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_metadata_spec.rb +102 -1
- data/microservices/CFDP/spec/models/cfdp_pdu_user_ops_spec.rb +562 -0
- data/microservices/CFDP/spec/models/cfdp_receive_transaction_spec.rb +598 -0
- data/microservices/CFDP/spec/models/cfdp_transaction_spec.rb +331 -0
- data/microservices/CFDP/spec/models/cfdp_user_spec.rb +575 -0
- metadata +15 -6
@@ -0,0 +1,331 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
# Copyright 2025 OpenC3, Inc.
|
4
|
+
# All Rights Reserved.
|
5
|
+
#
|
6
|
+
# Licensed for Evaluation and Educational Use
|
7
|
+
#
|
8
|
+
# This file may only be used commercially under the terms of a commercial license
|
9
|
+
# purchased from OpenC3, Inc.
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
14
|
+
#
|
15
|
+
# The development of this software was funded in-whole or in-part by Sandia National Laboratories.
|
16
|
+
# See https://github.com/OpenC3/openc3-cosmos-cfdp/pull/12 for details
|
17
|
+
|
18
|
+
require 'rails_helper'
|
19
|
+
|
20
|
+
RSpec.describe CfdpTransaction do
|
21
|
+
before(:each) do
|
22
|
+
# Mock CfdpTopic
|
23
|
+
allow(CfdpTopic).to receive(:write_indication)
|
24
|
+
|
25
|
+
# Mock CfdpMib
|
26
|
+
@source_entity = {
|
27
|
+
'id' => 1,
|
28
|
+
'name' => 'SOURCE',
|
29
|
+
'fault_handler' => {
|
30
|
+
'NO_ERROR' => 'IGNORE_ERROR',
|
31
|
+
'FILESTORE_REJECTION' => 'ISSUE_NOTICE_OF_CANCELLATION',
|
32
|
+
'FILE_CHECKSUM_FAILURE' => 'ISSUE_NOTICE_OF_SUSPENSION',
|
33
|
+
'FILE_SIZE_ERROR' => 'ABANDON_TRANSACTION',
|
34
|
+
'CHECK_LIMIT_REACHED' => 'IGNORE_ERROR'
|
35
|
+
},
|
36
|
+
'suspended_indication' => true,
|
37
|
+
'resume_indication' => true,
|
38
|
+
'keep_alive_interval' => 5
|
39
|
+
}
|
40
|
+
|
41
|
+
allow(CfdpMib).to receive(:source_entity).and_return(@source_entity)
|
42
|
+
|
43
|
+
# Mock logging
|
44
|
+
allow(OpenC3::Logger).to receive(:info)
|
45
|
+
allow(OpenC3::Logger).to receive(:error)
|
46
|
+
|
47
|
+
# Mock Api module
|
48
|
+
allow_any_instance_of(CfdpTransaction).to receive(:cmd)
|
49
|
+
|
50
|
+
ENV['OPENC3_SCOPE'] = 'DEFAULT'
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "initialize" do
|
54
|
+
it "initializes with default values" do
|
55
|
+
transaction = CfdpTransaction.new
|
56
|
+
|
57
|
+
expect(transaction.frozen).to be false
|
58
|
+
expect(transaction.state).to eq("ACTIVE")
|
59
|
+
expect(transaction.transaction_status).to eq("ACTIVE")
|
60
|
+
expect(transaction.progress).to eq(0)
|
61
|
+
expect(transaction.condition_code).to eq("NO_ERROR")
|
62
|
+
expect(transaction.delivery_code).to be_nil
|
63
|
+
expect(transaction.instance_variable_get(:@metadata_pdu_hash)).to be_nil
|
64
|
+
expect(transaction.instance_variable_get(:@metadata_pdu_count)).to eq(0)
|
65
|
+
expect(transaction.proxy_response_info).to be_nil
|
66
|
+
expect(transaction.proxy_response_needed).to be false
|
67
|
+
expect(transaction.instance_variable_get(:@source_file_name)).to be_nil
|
68
|
+
expect(transaction.instance_variable_get(:@destination_file_name)).to be_nil
|
69
|
+
expect(transaction.create_time).to be_a(Time)
|
70
|
+
expect(transaction.complete_time).to be_nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "class methods" do
|
75
|
+
it "builds a transaction id" do
|
76
|
+
id = CfdpTransaction.build_transaction_id(1, 123)
|
77
|
+
expect(id).to eq("1__123")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "instance methods" do
|
82
|
+
let(:transaction) { CfdpTransaction.new }
|
83
|
+
|
84
|
+
describe "suspend" do
|
85
|
+
it "suspends an active transaction" do
|
86
|
+
transaction.suspend
|
87
|
+
|
88
|
+
expect(transaction.state).to eq("SUSPENDED")
|
89
|
+
expect(transaction.condition_code).to eq("SUSPEND_REQUEST_RECEIVED")
|
90
|
+
expect(CfdpTopic).to have_received(:write_indication).with("Suspended", hash_including(transaction_id: nil, condition_code: "SUSPEND_REQUEST_RECEIVED"))
|
91
|
+
end
|
92
|
+
|
93
|
+
it "does nothing if transaction is not active" do
|
94
|
+
transaction.instance_variable_set(:@state, "FINISHED")
|
95
|
+
transaction.suspend
|
96
|
+
|
97
|
+
expect(transaction.state).to eq("FINISHED")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "resume" do
|
102
|
+
it "resumes a suspended transaction" do
|
103
|
+
transaction.instance_variable_set(:@state, "SUSPENDED")
|
104
|
+
transaction.resume
|
105
|
+
|
106
|
+
expect(transaction.state).to eq("ACTIVE")
|
107
|
+
expect(transaction.condition_code).to eq("NO_ERROR")
|
108
|
+
expect(CfdpTopic).to have_received(:write_indication).with("Resumed", hash_including(transaction_id: nil, progress: 0))
|
109
|
+
end
|
110
|
+
|
111
|
+
it "does nothing if transaction is not suspended" do
|
112
|
+
transaction.resume
|
113
|
+
|
114
|
+
expect(transaction.state).to eq("ACTIVE")
|
115
|
+
expect(CfdpTopic).not_to have_received(:write_indication).with("Resumed", any_args)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "cancel" do
|
120
|
+
it "cancels an active transaction" do
|
121
|
+
transaction.cancel
|
122
|
+
|
123
|
+
expect(transaction.state).to eq("CANCELED")
|
124
|
+
expect(transaction.transaction_status).to eq("TERMINATED")
|
125
|
+
expect(transaction.condition_code).to eq("CANCEL_REQUEST_RECEIVED")
|
126
|
+
expect(transaction.complete_time).to be_a(Time)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "cancels with a canceling entity id" do
|
130
|
+
transaction.cancel(3)
|
131
|
+
|
132
|
+
expect(transaction.state).to eq("CANCELED")
|
133
|
+
expect(transaction.instance_variable_get(:@canceling_entity_id)).to eq(3)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "does nothing if transaction is already finished" do
|
137
|
+
transaction.instance_variable_set(:@state, "FINISHED")
|
138
|
+
original_time = Time.now.utc - 10
|
139
|
+
transaction.instance_variable_set(:@complete_time, original_time)
|
140
|
+
|
141
|
+
transaction.cancel
|
142
|
+
|
143
|
+
expect(transaction.state).to eq("FINISHED")
|
144
|
+
expect(transaction.complete_time).to eq(original_time)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "abandon" do
|
149
|
+
it "abandons an active transaction" do
|
150
|
+
transaction.abandon
|
151
|
+
|
152
|
+
expect(transaction.state).to eq("ABANDONED")
|
153
|
+
expect(transaction.transaction_status).to eq("TERMINATED")
|
154
|
+
expect(CfdpTopic).to have_received(:write_indication).with("Abandoned", hash_including(transaction_id: nil, condition_code: "NO_ERROR", progress: 0))
|
155
|
+
expect(transaction.complete_time).to be_a(Time)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "does nothing if transaction is already finished" do
|
159
|
+
transaction.instance_variable_set(:@state, "FINISHED")
|
160
|
+
original_time = Time.now.utc - 10
|
161
|
+
transaction.instance_variable_set(:@complete_time, original_time)
|
162
|
+
|
163
|
+
transaction.abandon
|
164
|
+
|
165
|
+
expect(transaction.state).to eq("FINISHED")
|
166
|
+
expect(transaction.complete_time).to eq(original_time)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "report" do
|
171
|
+
it "sends a report indication" do
|
172
|
+
transaction.report
|
173
|
+
|
174
|
+
expect(CfdpTopic).to have_received(:write_indication).with("Report", hash_including(transaction_id: nil))
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "freeze and unfreeze" do
|
179
|
+
it "freezes the transaction" do
|
180
|
+
transaction.freeze
|
181
|
+
expect(transaction.instance_variable_get(:@freeze)).to be true
|
182
|
+
end
|
183
|
+
|
184
|
+
it "unfreezes the transaction" do
|
185
|
+
transaction.instance_variable_set(:@freeze, true)
|
186
|
+
transaction.unfreeze
|
187
|
+
expect(transaction.instance_variable_get(:@freeze)).to be false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
describe "build_report" do
|
192
|
+
it "generates a JSON report" do
|
193
|
+
transaction.instance_variable_set(:@id, "1__123")
|
194
|
+
report = transaction.build_report
|
195
|
+
|
196
|
+
expect(report).to be_a(String)
|
197
|
+
|
198
|
+
# Parse and verify JSON structure
|
199
|
+
json = JSON.parse(report)
|
200
|
+
expect(json["id"]).to eq("1__123")
|
201
|
+
expect(json["state"]).to eq("ACTIVE")
|
202
|
+
expect(json["transaction_status"]).to eq("ACTIVE")
|
203
|
+
expect(json["progress"]).to eq(0)
|
204
|
+
expect(json["frozen"]).to be false
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "as_json" do
|
209
|
+
it "returns a hash representation of the transaction" do
|
210
|
+
transaction.instance_variable_set(:@id, "1__123")
|
211
|
+
transaction.instance_variable_set(:@source_file_name, "source.txt")
|
212
|
+
transaction.instance_variable_set(:@destination_file_name, "dest.txt")
|
213
|
+
|
214
|
+
json = transaction.as_json
|
215
|
+
|
216
|
+
expect(json).to be_a(Hash)
|
217
|
+
expect(json["id"]).to eq("1__123")
|
218
|
+
expect(json["state"]).to eq("ACTIVE")
|
219
|
+
expect(json["source_file_name"]).to eq("source.txt")
|
220
|
+
expect(json["destination_file_name"]).to eq("dest.txt")
|
221
|
+
expect(json["create_time"]).to be_a(String)
|
222
|
+
expect(json["complete_time"]).to be_nil
|
223
|
+
end
|
224
|
+
|
225
|
+
it "includes complete_time when available" do
|
226
|
+
transaction.instance_variable_set(:@complete_time, Time.now.utc)
|
227
|
+
|
228
|
+
json = transaction.as_json
|
229
|
+
|
230
|
+
expect(json["complete_time"]).to be_a(String)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
describe "handle_fault" do
|
235
|
+
it "handles ISSUE_NOTICE_OF_CANCELLATION response" do
|
236
|
+
transaction.instance_variable_set(:@condition_code, "FILESTORE_REJECTION")
|
237
|
+
|
238
|
+
expect(transaction).to receive(:cancel)
|
239
|
+
transaction.handle_fault
|
240
|
+
end
|
241
|
+
|
242
|
+
it "handles ISSUE_NOTICE_OF_SUSPENSION response" do
|
243
|
+
transaction.instance_variable_set(:@condition_code, "FILE_CHECKSUM_FAILURE")
|
244
|
+
|
245
|
+
expect(transaction).to receive(:suspend)
|
246
|
+
transaction.handle_fault
|
247
|
+
end
|
248
|
+
|
249
|
+
it "handles ABANDON_TRANSACTION response" do
|
250
|
+
transaction.instance_variable_set(:@condition_code, "FILE_SIZE_ERROR")
|
251
|
+
|
252
|
+
expect(transaction).to receive(:abandon)
|
253
|
+
transaction.handle_fault
|
254
|
+
end
|
255
|
+
|
256
|
+
it "handles IGNORE_ERROR response" do
|
257
|
+
transaction.instance_variable_set(:@condition_code, "CHECK_LIMIT_REACHED")
|
258
|
+
|
259
|
+
expect(transaction).to receive(:ignore_fault)
|
260
|
+
transaction.handle_fault
|
261
|
+
end
|
262
|
+
|
263
|
+
it "uses fault handler overrides" do
|
264
|
+
transaction.instance_variable_set(:@condition_code, "CHECK_LIMIT_REACHED")
|
265
|
+
transaction.instance_variable_set(:@fault_handler_overrides, {"CHECK_LIMIT_REACHED" => "ISSUE_NOTICE_OF_CANCELLATION"})
|
266
|
+
|
267
|
+
expect(transaction).to receive(:cancel)
|
268
|
+
transaction.handle_fault
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe "ignore_fault" do
|
273
|
+
it "sends a fault indication" do
|
274
|
+
transaction.instance_variable_set(:@condition_code, "FILE_SIZE_ERROR")
|
275
|
+
|
276
|
+
transaction.ignore_fault
|
277
|
+
|
278
|
+
expect(CfdpTopic).to have_received(:write_indication).with("Fault", hash_including(transaction_id: nil, condition_code: "FILE_SIZE_ERROR", progress: 0))
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe "get_checksum" do
|
283
|
+
it "returns a CfdpChecksum for type 0" do
|
284
|
+
checksum = transaction.get_checksum(0)
|
285
|
+
expect(checksum).to be_a(CfdpChecksum)
|
286
|
+
end
|
287
|
+
|
288
|
+
it "returns a CfdpCrcChecksum for type 1" do
|
289
|
+
checksum = transaction.get_checksum(1)
|
290
|
+
expect(checksum).to be_a(CfdpCrcChecksum)
|
291
|
+
end
|
292
|
+
|
293
|
+
it "returns a CfdpCrcChecksum for type 2" do
|
294
|
+
checksum = transaction.get_checksum(2)
|
295
|
+
expect(checksum).to be_a(CfdpCrcChecksum)
|
296
|
+
end
|
297
|
+
|
298
|
+
it "returns a CfdpCrcChecksum for type 3" do
|
299
|
+
checksum = transaction.get_checksum(3)
|
300
|
+
expect(checksum).to be_a(CfdpCrcChecksum)
|
301
|
+
end
|
302
|
+
|
303
|
+
it "returns a CfdpNullChecksum for type 15" do
|
304
|
+
checksum = transaction.get_checksum(15)
|
305
|
+
expect(checksum).to be_a(CfdpNullChecksum)
|
306
|
+
end
|
307
|
+
|
308
|
+
it "returns nil for unknown checksum types" do
|
309
|
+
checksum = transaction.get_checksum(10)
|
310
|
+
expect(checksum).to be_nil
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "cfdp_cmd" do
|
315
|
+
it "sends a command with the correct parameters" do
|
316
|
+
entity = {'cmd_delay' => 0.1}
|
317
|
+
|
318
|
+
expect(transaction).to receive(:cmd).with('TARGET', 'PACKET', {'PARAM' => 'value'}, scope: 'DEFAULT')
|
319
|
+
|
320
|
+
transaction.cfdp_cmd(entity, 'TARGET', 'PACKET', {'PARAM' => 'value'}, scope: 'DEFAULT')
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe "update" do
|
325
|
+
it "does nothing in the base class" do
|
326
|
+
# Just verifying it doesn't raise an error
|
327
|
+
transaction.update
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|