1#![allow(clippy::missing_inline_in_public_items)]
4use crate::lib_api::{decode_record, encode_record};
9use crate::options::{DecodeOptions, EncodeOptions};
10use copybook_core::{Error, ErrorCode, Result, Schema};
11use copybook_determinism::compare_outputs;
12
13pub use copybook_determinism::{ByteDiff, DeterminismMode, DeterminismResult};
14
15fn serialize_json(value: &serde_json::Value, context: &str) -> Result<Vec<u8>> {
16 serde_json::to_vec(value).map_err(|e| {
17 Error::new(
18 ErrorCode::CBKC201_JSON_WRITE_ERROR,
19 format!("Failed to serialize {context}: {e}"),
20 )
21 })
22}
23
24pub fn check_decode_determinism(
30 schema: &Schema,
31 data: &[u8],
32 options: &DecodeOptions,
33) -> Result<DeterminismResult> {
34 let value1 = decode_record(schema, data, options)?;
35 let value2 = decode_record(schema, data, options)?;
36
37 let json1 = serialize_json(&value1, "first decode result")?;
38 let json2 = serialize_json(&value2, "second decode result")?;
39
40 Ok(compare_outputs(DeterminismMode::DecodeOnly, &json1, &json2))
41}
42
43pub fn check_encode_determinism(
49 schema: &Schema,
50 json_data: &serde_json::Value,
51 options: &EncodeOptions,
52) -> Result<DeterminismResult> {
53 let binary1 = encode_record(schema, json_data, options)?;
54 let binary2 = encode_record(schema, json_data, options)?;
55
56 Ok(compare_outputs(
57 DeterminismMode::EncodeOnly,
58 &binary1,
59 &binary2,
60 ))
61}
62
63pub fn check_round_trip_determinism(
69 schema: &Schema,
70 data: &[u8],
71 decode_opts: &DecodeOptions,
72 encode_opts: &EncodeOptions,
73) -> Result<DeterminismResult> {
74 let json1 = decode_record(schema, data, decode_opts)?;
75 let binary = encode_record(schema, &json1, encode_opts)?;
76 let json2 = decode_record(schema, &binary, decode_opts)?;
77
78 let serialized1 = serialize_json(&json1, "first round-trip decode result")?;
79 let serialized2 = serialize_json(&json2, "second round-trip decode result")?;
80
81 Ok(compare_outputs(
82 DeterminismMode::RoundTrip,
83 &serialized1,
84 &serialized2,
85 ))
86}
87
88#[cfg(test)]
89#[allow(clippy::expect_used)]
90#[allow(clippy::unwrap_used)]
91mod tests {
92 use super::*;
93 use crate::options::{Codepage, RecordFormat};
94 use copybook_core::parse_copybook;
95
96 fn decode_opts() -> DecodeOptions {
97 DecodeOptions::new().with_codepage(Codepage::CP037)
98 }
99
100 fn encode_opts() -> EncodeOptions {
101 EncodeOptions::new()
102 .with_codepage(Codepage::CP037)
103 .with_format(RecordFormat::Fixed)
104 }
105
106 #[test]
107 fn decode_deterministic_for_display_schema() {
108 let copybook = r"
109 01 RECORD.
110 05 FIELD-A PIC X(10).
111 ";
112 let schema = parse_copybook(copybook).expect("parse copybook");
113
114 let data: Vec<u8> = vec![0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xD1];
115
116 let result =
117 check_decode_determinism(&schema, &data, &decode_opts()).expect("determinism check");
118
119 assert!(
120 result.is_deterministic,
121 "Expected deterministic decode for DISPLAY-only schema"
122 );
123 assert_eq!(result.mode, DeterminismMode::DecodeOnly);
124 assert!(result.byte_differences.is_none());
125 assert_eq!(result.diff_count(), 0);
126 assert!(result.passed());
127 }
128
129 #[test]
130 fn decode_deterministic_for_comp3_schema() {
131 let copybook = r"
132 01 RECORD.
133 05 AMOUNT PIC S9(7)V99 COMP-3.
134 ";
135 let schema = parse_copybook(copybook).expect("parse copybook");
136
137 let data = vec![0x12, 0x34, 0x56, 0x78, 0x9C];
138
139 let result =
140 check_decode_determinism(&schema, &data, &decode_opts()).expect("determinism check");
141
142 assert!(
143 result.is_deterministic,
144 "Expected deterministic decode for COMP-3 schema"
145 );
146 assert!(result.passed());
147 }
148
149 #[test]
150 fn encode_deterministic_for_display_schema() {
151 let copybook = r"
152 01 RECORD.
153 05 FIELD-A PIC X(5).
154 ";
155 let schema = parse_copybook(copybook).expect("parse copybook");
156 let json = serde_json::json!({"FIELD-A": "HELLO"});
157
158 let result =
159 check_encode_determinism(&schema, &json, &encode_opts()).expect("determinism check");
160
161 assert!(
162 result.is_deterministic,
163 "Expected deterministic encode for DISPLAY-only schema"
164 );
165 assert_eq!(result.mode, DeterminismMode::EncodeOnly);
166 assert!(result.byte_differences.is_none());
167 }
168
169 #[test]
170 fn round_trip_deterministic() {
171 let copybook = r"
172 01 RECORD.
173 05 NAME PIC X(10).
174 05 AGE PIC 9(3).
175 ";
176 let schema = parse_copybook(copybook).expect("parse copybook");
177
178 let data: Vec<u8> = vec![
179 0xD1, 0xD6, 0xC8, 0xD5, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0xF1, 0xF2, 0xF3,
180 ];
181
182 let result = check_round_trip_determinism(&schema, &data, &decode_opts(), &encode_opts())
183 .expect("round-trip check");
184
185 assert!(result.is_deterministic, "Expected deterministic round-trip");
186 assert_eq!(result.mode, DeterminismMode::RoundTrip);
187 }
188
189 #[test]
190 fn detect_json_serialization_nondeterminism() {
191 let json1 = serde_json::json!({"FIELD": "VALUE1"});
192 let json2 = serde_json::json!({"FIELD": "VALUE2"});
193
194 let bytes1 = serde_json::to_vec(&json1).expect("serialize json1");
195 let bytes2 = serde_json::to_vec(&json2).expect("serialize json2");
196
197 let result = compare_outputs(DeterminismMode::DecodeOnly, &bytes1, &bytes2);
198 assert!(!result.is_deterministic);
199 assert!(result.diff_count() > 0);
200 }
201
202 #[test]
203 fn decode_error_propagates_correctly() {
204 let copybook = r"
205 01 RECORD.
206 05 AMOUNT PIC S9(7)V99 COMP-3.
207 ";
208 let schema = parse_copybook(copybook).expect("parse copybook");
209
210 let truncated_data = vec![0x12, 0x34];
211
212 let result = check_decode_determinism(&schema, &truncated_data, &decode_opts());
213
214 assert!(
215 result.is_err(),
216 "Should return error for truncated COMP-3 data"
217 );
218 }
219
220 #[test]
221 fn encode_error_propagates_correctly() {
222 let copybook = r"
223 01 RECORD.
224 05 FIELD PIC 9(5).
225 ";
226 let schema = parse_copybook(copybook).expect("parse copybook");
227
228 let invalid_json = serde_json::json!({"FIELD": "NOT_A_NUMBER"});
229
230 let result = check_encode_determinism(&schema, &invalid_json, &encode_opts());
231
232 assert!(
233 result.is_err(),
234 "Should return error for type mismatch in encoding"
235 );
236 }
237
238 #[test]
239 fn round_trip_error_propagates() {
240 let copybook = r"
241 01 RECORD.
242 05 AMOUNT PIC S9(7)V99 COMP-3.
243 ";
244 let schema = parse_copybook(copybook).expect("parse copybook");
245
246 let bad_data = vec![0x12, 0x34];
247
248 let result =
249 check_round_trip_determinism(&schema, &bad_data, &decode_opts(), &encode_opts());
250
251 assert!(
252 result.is_err(),
253 "Should return error for truncated data in round-trip"
254 );
255 }
256
257 #[test]
258 fn insufficient_data_handling_is_stable() {
259 let copybook = r"
260 01 RECORD.
261 05 FIELD PIC X(5).
262 ";
263 let schema = parse_copybook(copybook).expect("parse copybook");
264
265 let insufficient_data = vec![0x40, 0x40, 0x40];
266
267 let result = check_decode_determinism(&schema, &insufficient_data, &decode_opts());
268
269 if let Ok(det_result) = result {
270 assert!(
271 det_result.is_deterministic,
272 "If insufficient data is handled, it must be deterministic"
273 );
274 }
275 }
276}