1use std::io::{Read, Seek, SeekFrom};
4
5use encoding_rs::{Encoding, UTF_8};
6use runmat_builtins::{CharArray, Tensor, Value};
7use runmat_macros::runtime_builtin;
8
9use crate::builtins::common::spec::{
10 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11 ReductionNaN, ResidencyPolicy, ShapeRequirements,
12};
13use crate::builtins::io::filetext::registry;
14use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
15use runmat_filesystem::File;
16
17const INVALID_IDENTIFIER_MESSAGE: &str =
18 "Invalid file identifier. Use fopen to generate a valid file ID.";
19const BUILTIN_NAME: &str = "fgets";
20
21#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::filetext::fgets")]
22pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
23 name: "fgets",
24 op_kind: GpuOpKind::Custom("file-io"),
25 supported_precisions: &[],
26 broadcast: BroadcastSemantics::None,
27 provider_hooks: &[],
28 constant_strategy: ConstantStrategy::InlineLiteral,
29 residency: ResidencyPolicy::GatherImmediately,
30 nan_mode: ReductionNaN::Include,
31 two_pass_threshold: None,
32 workgroup_size: None,
33 accepts_nan_mode: false,
34 notes: "Host-only file I/O; arguments gathered from the GPU when necessary.",
35};
36
37fn fgets_error(message: impl Into<String>) -> RuntimeError {
38 build_runtime_error(message)
39 .with_builtin(BUILTIN_NAME)
40 .build()
41}
42
43fn map_control_flow(err: RuntimeError) -> RuntimeError {
44 let message = err.message().to_string();
45 let identifier = err.identifier().map(|value| value.to_string());
46 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {message}"))
47 .with_builtin(BUILTIN_NAME)
48 .with_source(err);
49 if let Some(identifier) = identifier {
50 builder = builder.with_identifier(identifier);
51 }
52 builder.build()
53}
54
55#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::filetext::fgets")]
56pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
57 name: "fgets",
58 shape: ShapeRequirements::Any,
59 constant_strategy: ConstantStrategy::InlineLiteral,
60 elementwise: None,
61 reduction: None,
62 emits_nan: false,
63 notes: "File I/O calls are not eligible for fusion.",
64};
65
66#[runtime_builtin(
67 name = "fgets",
68 category = "io/filetext",
69 summary = "Read the next line from a file, including newline characters.",
70 keywords = "fgets,file,io,line,newline",
71 accel = "cpu",
72 type_resolver(crate::builtins::io::type_resolvers::fgets_type),
73 builtin_path = "crate::builtins::io::filetext::fgets"
74)]
75async fn fgets_builtin(fid: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
76 let eval = evaluate(&fid, &rest).await?;
77 if let Some(out_count) = crate::output_count::current_output_count() {
78 if out_count == 0 {
79 return Ok(Value::OutputList(Vec::new()));
80 }
81 return Ok(crate::output_count::output_list_with_padding(
82 out_count,
83 eval.outputs(),
84 ));
85 }
86 Ok(eval.first_output())
87}
88
89#[derive(Clone, Debug)]
90pub struct FgetsEval {
91 line: Value,
92 terminators: Value,
93}
94
95impl FgetsEval {
96 fn new(line: Value, terminators: Value) -> Self {
97 Self { line, terminators }
98 }
99
100 fn end_of_file() -> Self {
101 Self {
102 line: Value::Num(-1.0),
103 terminators: Value::Num(-1.0),
104 }
105 }
106
107 pub fn first_output(&self) -> Value {
108 self.line.clone()
109 }
110
111 pub fn outputs(&self) -> Vec<Value> {
112 vec![self.line.clone(), self.terminators.clone()]
113 }
114}
115
116pub async fn evaluate(fid_value: &Value, rest: &[Value]) -> BuiltinResult<FgetsEval> {
117 if rest.len() > 1 {
118 return Err(fgets_error("fgets: too many input arguments"));
119 }
120
121 let fid_host = gather_value(fid_value).await?;
122 let fid = parse_fid(&fid_host)?;
123 if fid < 0 {
124 return Err(fgets_error("fgets: file identifier must be non-negative"));
125 }
126 if fid < 3 {
127 return Err(fgets_error(
128 "fgets: standard input/output identifiers are not supported yet",
129 ));
130 }
131
132 let info = registry::info_for(fid)
133 .ok_or_else(|| fgets_error(format!("fgets: {INVALID_IDENTIFIER_MESSAGE}")))?;
134 if !permission_allows_read(&info.permission) {
135 return Err(fgets_error(
136 "fgets: file identifier is not open for reading",
137 ));
138 }
139 let handle = registry::take_handle(fid)
140 .ok_or_else(|| fgets_error(format!("fgets: {INVALID_IDENTIFIER_MESSAGE}")))?;
141 let mut file = handle
142 .lock()
143 .map_err(|_| fgets_error("fgets: failed to lock file handle (poisoned mutex)"))?;
144
145 let nchar_limit = parse_nchar(rest).await?;
146 let max_bytes = apply_matlab_nchar_limit(nchar_limit);
147 let read = read_line(&mut file, max_bytes)?;
148 if read.eof_before_any {
149 return Ok(FgetsEval::end_of_file());
150 }
151
152 let encoding = if info.encoding.trim().is_empty() {
153 "UTF-8".to_string()
154 } else {
155 info.encoding.clone()
156 };
157
158 let line_value = bytes_to_char_array(&read.data, &encoding)?;
159 let terminators_value = if read.terminators.is_empty() {
160 empty_numeric_row()
161 } else {
162 numeric_row(&read.terminators)?
163 };
164
165 Ok(FgetsEval::new(line_value, terminators_value))
166}
167
168async fn gather_value(value: &Value) -> BuiltinResult<Value> {
169 gather_if_needed_async(value)
170 .await
171 .map_err(map_control_flow)
172}
173
174fn parse_fid(value: &Value) -> BuiltinResult<i32> {
175 match value {
176 Value::Num(n) => {
177 if !n.is_finite() {
178 return Err(fgets_error("fgets: file identifier must be finite"));
179 }
180 if (n.fract()).abs() > f64::EPSILON {
181 return Err(fgets_error(
182 "fgets: file identifier must be an integer scalar",
183 ));
184 }
185 Ok(*n as i32)
186 }
187 Value::Int(i) => Ok(i.to_i64() as i32),
188 Value::Tensor(t) if t.data.len() == 1 => {
189 let n = t.data[0];
190 if !n.is_finite() {
191 return Err(fgets_error("fgets: file identifier must be finite"));
192 }
193 if (n.fract()).abs() > f64::EPSILON {
194 return Err(fgets_error(
195 "fgets: file identifier must be an integer scalar",
196 ));
197 }
198 Ok(n as i32)
199 }
200 _ => Err(fgets_error(
201 "fgets: file identifier must be a numeric scalar",
202 )),
203 }
204}
205
206async fn parse_nchar(args: &[Value]) -> BuiltinResult<Option<usize>> {
207 if args.is_empty() {
208 return Ok(None);
209 }
210 let value = gather_value(&args[0]).await?;
211 match value {
212 Value::Num(n) => {
213 if !n.is_finite() {
214 if n.is_sign_positive() {
215 return Ok(None);
216 }
217 return Err(fgets_error(
218 "fgets: nchar must be a non-negative integer scalar",
219 ));
220 }
221 if n < 0.0 {
222 return Err(fgets_error(
223 "fgets: nchar must be a non-negative integer scalar",
224 ));
225 }
226 if (n.fract()).abs() > f64::EPSILON {
227 return Err(fgets_error(
228 "fgets: nchar must be a non-negative integer scalar",
229 ));
230 }
231 Ok(Some(n as usize))
232 }
233 Value::Int(i) => {
234 let raw = i.to_i64();
235 if raw < 0 {
236 return Err(fgets_error(
237 "fgets: nchar must be a non-negative integer scalar",
238 ));
239 }
240 Ok(Some(raw as usize))
241 }
242 Value::Tensor(t) if t.data.len() == 1 => {
243 let n = t.data[0];
244 if !n.is_finite() {
245 if n.is_sign_positive() {
246 return Ok(None);
247 }
248 return Err(fgets_error(
249 "fgets: nchar must be a non-negative integer scalar",
250 ));
251 }
252 if n < 0.0 {
253 return Err(fgets_error(
254 "fgets: nchar must be a non-negative integer scalar",
255 ));
256 }
257 if (n.fract()).abs() > f64::EPSILON {
258 return Err(fgets_error(
259 "fgets: nchar must be a non-negative integer scalar",
260 ));
261 }
262 Ok(Some(n as usize))
263 }
264 _ => Err(fgets_error(
265 "fgets: nchar must be a non-negative integer scalar",
266 )),
267 }
268}
269
270fn permission_allows_read(permission: &str) -> bool {
271 let lower = permission.to_ascii_lowercase();
272 lower.starts_with('r') || lower.contains('+')
273}
274
275fn apply_matlab_nchar_limit(nchar_limit: Option<usize>) -> Option<usize> {
276 nchar_limit.map(|nchar| nchar.saturating_sub(1))
277}
278
279struct LineRead {
280 data: Vec<u8>,
281 terminators: Vec<u8>,
282 eof_before_any: bool,
283}
284
285fn read_line(file: &mut File, limit: Option<usize>) -> BuiltinResult<LineRead> {
286 let mut data = Vec::new();
287 let mut terminators = Vec::new();
288 let mut eof_before_any = false;
289
290 let max_bytes = limit.unwrap_or(usize::MAX);
291 if max_bytes == 0 {
292 return Ok(LineRead {
293 data,
294 terminators,
295 eof_before_any,
296 });
297 }
298
299 let mut first_attempt = true;
300 let mut buffer = [0u8; 1];
301 loop {
302 if data.len() >= max_bytes {
303 break;
304 }
305
306 let read = file.read(&mut buffer).map_err(|err| {
307 build_runtime_error(format!("fgets: failed to read from file: {err}"))
308 .with_builtin(BUILTIN_NAME)
309 .with_source(err)
310 .build()
311 })?;
312 if read == 0 {
313 if data.is_empty() && first_attempt {
314 eof_before_any = true;
315 }
316 break;
317 }
318 first_attempt = false;
319 let byte = buffer[0];
320
321 if byte == b'\n' {
322 if data.len().saturating_add(1) > max_bytes {
323 file.seek(SeekFrom::Current(-1)).map_err(|err| {
324 build_runtime_error(format!("fgets: failed to seek in file: {err}"))
325 .with_builtin(BUILTIN_NAME)
326 .with_source(err)
327 .build()
328 })?;
329 } else {
330 data.push(b'\n');
331 terminators.push(b'\n');
332 }
333 break;
334 } else if byte == b'\r' {
335 let mut newline = [0u8; 2];
336 newline[0] = b'\r';
337 let mut newline_len = 1usize;
338 let mut consumed = 1i64;
339
340 let mut next = [0u8; 1];
341 let read_next = file.read(&mut next).map_err(|err| {
342 build_runtime_error(format!("fgets: failed to read from file: {err}"))
343 .with_builtin(BUILTIN_NAME)
344 .with_source(err)
345 .build()
346 })?;
347 if read_next > 0 {
348 if next[0] == b'\n' {
349 newline[1] = b'\n';
350 newline_len = 2;
351 consumed = 2;
352 } else {
353 file.seek(SeekFrom::Current(-1)).map_err(|err| {
354 build_runtime_error(format!("fgets: failed to seek in file: {err}"))
355 .with_builtin(BUILTIN_NAME)
356 .with_source(err)
357 .build()
358 })?;
359 }
360 }
361
362 if data.len().saturating_add(newline_len) > max_bytes {
363 file.seek(SeekFrom::Current(-consumed)).map_err(|err| {
364 build_runtime_error(format!("fgets: failed to seek in file: {err}"))
365 .with_builtin(BUILTIN_NAME)
366 .with_source(err)
367 .build()
368 })?;
369 } else {
370 data.extend_from_slice(&newline[..newline_len]);
371 terminators.extend_from_slice(&newline[..newline_len]);
372 }
373 break;
374 } else {
375 data.push(byte);
376 }
377 }
378
379 Ok(LineRead {
380 data,
381 terminators,
382 eof_before_any,
383 })
384}
385
386fn bytes_to_char_array(bytes: &[u8], encoding: &str) -> BuiltinResult<Value> {
387 let chars = decode_bytes(bytes, encoding)?;
388 let cols = chars.len();
389 let char_array = CharArray::new(chars, 1, cols)
390 .map_err(|e| fgets_error(format!("fgets: failed to build char array: {e}")))?;
391 Ok(Value::CharArray(char_array))
392}
393
394fn decode_bytes(bytes: &[u8], encoding: &str) -> BuiltinResult<Vec<char>> {
395 let label = encoding.trim();
396 if label.is_empty() || label.eq_ignore_ascii_case("utf-8") || label.eq_ignore_ascii_case("utf8")
397 {
398 return decode_with_encoding(bytes, UTF_8);
399 }
400 if label.eq_ignore_ascii_case("binary") {
401 return Ok(bytes
402 .iter()
403 .map(|&b| char::from_u32(b as u32).unwrap())
404 .collect());
405 }
406 if label.eq_ignore_ascii_case("latin1")
407 || label.eq_ignore_ascii_case("latin-1")
408 || label.eq_ignore_ascii_case("iso-8859-1")
409 {
410 return Ok(bytes
411 .iter()
412 .map(|&b| char::from_u32(b as u32).unwrap())
413 .collect());
414 }
415 if label.eq_ignore_ascii_case("windows-1252") || label.eq_ignore_ascii_case("cp1252") {
416 return decode_with_encoding(bytes, encoding_rs::WINDOWS_1252);
417 }
418 if label.eq_ignore_ascii_case("shift_jis")
419 || label.eq_ignore_ascii_case("shift-jis")
420 || label.eq_ignore_ascii_case("sjis")
421 {
422 return decode_with_encoding(bytes, encoding_rs::SHIFT_JIS);
423 }
424 if label.eq_ignore_ascii_case("us-ascii")
425 || label.eq_ignore_ascii_case("ascii")
426 || label.eq_ignore_ascii_case("us_ascii")
427 || label.eq_ignore_ascii_case("usascii")
428 {
429 return decode_ascii(bytes);
430 }
431 if label.eq_ignore_ascii_case("system") {
432 let fallback = system_default_encoding_label();
433 if fallback.eq_ignore_ascii_case("binary") {
434 return Ok(bytes
435 .iter()
436 .map(|&b| char::from_u32(b as u32).unwrap())
437 .collect());
438 }
439 return decode_bytes(bytes, fallback);
440 }
441
442 if let Some(enc) = Encoding::for_label(label.as_bytes()) {
443 return decode_with_encoding(bytes, enc);
444 }
445
446 Err(fgets_error(format!(
447 "fgets: unsupported encoding '{encoding}'"
448 )))
449}
450
451fn decode_with_encoding(bytes: &[u8], enc: &'static Encoding) -> BuiltinResult<Vec<char>> {
452 let (cow, _, had_errors) = enc.decode(bytes);
453 if had_errors {
454 return Err(fgets_error(format!(
455 "fgets: unable to decode bytes using encoding '{}'",
456 enc.name()
457 )));
458 }
459 Ok(cow.chars().collect())
460}
461
462fn decode_ascii(bytes: &[u8]) -> BuiltinResult<Vec<char>> {
463 if let Some(byte) = bytes.iter().find(|&&b| b > 0x7F) {
464 return Err(fgets_error(format!(
465 "fgets: byte value {} is outside the ASCII range",
466 byte
467 )));
468 }
469 Ok(bytes
470 .iter()
471 .map(|&b| char::from_u32(b as u32).unwrap())
472 .collect())
473}
474
475fn numeric_row(bytes: &[u8]) -> BuiltinResult<Value> {
476 let data: Vec<f64> = bytes.iter().map(|&b| b as f64).collect();
477 let tensor = Tensor::new(data, vec![1, bytes.len()])
478 .map_err(|e| fgets_error(format!("fgets: failed to construct numeric array: {e}")))?;
479 Ok(Value::Tensor(tensor))
480}
481
482fn empty_numeric_row() -> Value {
483 let tensor = Tensor::new(Vec::new(), vec![0, 0]).unwrap_or_else(|_| Tensor::zeros(vec![0, 0]));
484 Value::Tensor(tensor)
485}
486
487fn system_default_encoding_label() -> &'static str {
488 #[cfg(windows)]
489 {
490 "windows-1252"
491 }
492 #[cfg(not(windows))]
493 {
494 "utf-8"
495 }
496}
497
498#[cfg(test)]
499pub(crate) mod tests {
500 use super::*;
501 use crate::builtins::common::test_support;
502 use crate::builtins::io::filetext::{fopen, registry};
503 use crate::RuntimeError;
504 use runmat_accelerate_api::HostTensorView;
505 use runmat_builtins::IntValue;
506 use runmat_time::system_time_now;
507 use std::path::{Path, PathBuf};
508 use std::time::UNIX_EPOCH;
509
510 fn unwrap_error_message(err: RuntimeError) -> String {
511 err.message().to_string()
512 }
513
514 fn run_evaluate(fid_value: &Value, rest: &[Value]) -> BuiltinResult<FgetsEval> {
515 futures::executor::block_on(evaluate(fid_value, rest))
516 }
517
518 fn run_fopen(args: &[Value]) -> BuiltinResult<fopen::FopenEval> {
519 futures::executor::block_on(fopen::evaluate(args))
520 }
521
522 fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
523 registry::test_guard()
524 }
525
526 fn unique_path(prefix: &str) -> PathBuf {
527 let now = system_time_now()
528 .duration_since(UNIX_EPOCH)
529 .expect("time went backwards");
530 let filename = format!("{}_{}_{}.tmp", prefix, now.as_secs(), now.subsec_nanos());
531 std::env::temp_dir().join(filename)
532 }
533
534 fn fopen_path(path: &Path) -> FopenHandle {
535 let eval = run_fopen(&[Value::from(path.to_string_lossy().to_string())]).expect("fopen");
536 let open = eval.as_open().expect("open outputs");
537 assert!(open.fid >= 3.0);
538 FopenHandle {
539 fid: open.fid as i32,
540 }
541 }
542
543 struct FopenHandle {
544 fid: i32,
545 }
546
547 impl Drop for FopenHandle {
548 fn drop(&mut self) {
549 let _ = registry::close(self.fid);
550 }
551 }
552
553 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
554 #[test]
555 fn fgets_reads_line_with_newline() {
556 let _guard = registry_guard();
557 registry::reset_for_tests();
558 let path = unique_path("fgets_line");
559 test_support::fs::write(&path, "Hello world\nSecond line\n").unwrap();
560
561 let handle = fopen_path(&path);
562 let eval = run_evaluate(&Value::Num(handle.fid as f64), &[]).expect("fgets");
563 let line = eval.first_output();
564 match line {
565 Value::CharArray(ca) => {
566 let text: String = ca.data.iter().collect();
567 assert_eq!(text, "Hello world\n");
568 }
569 other => panic!("expected char array, got {other:?}"),
570 }
571 let ltout = eval.outputs()[1].clone();
572 match ltout {
573 Value::Tensor(t) => {
574 assert_eq!(t.data, vec![10.0]);
575 assert_eq!(t.shape, vec![1, 1]);
576 }
577 other => panic!("expected numeric tensor, got {other:?}"),
578 }
579
580 test_support::fs::remove_file(&path).unwrap();
581 }
582
583 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
584 #[test]
585 fn fgets_returns_minus_one_at_eof() {
586 let _guard = registry_guard();
587 registry::reset_for_tests();
588 let path = unique_path("fgets_eof");
589 test_support::fs::write(&path, "line\n").unwrap();
590 let handle = fopen_path(&path);
591
592 let _ = run_evaluate(&Value::Num(handle.fid as f64), &[]).expect("first read");
593 let eval = run_evaluate(&Value::Num(handle.fid as f64), &[]).expect("second read");
594 assert_eq!(eval.first_output(), Value::Num(-1.0));
595 assert_eq!(eval.outputs()[1], Value::Num(-1.0));
596
597 test_support::fs::remove_file(&path).unwrap();
598 }
599
600 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
601 #[test]
602 fn fgets_honours_nchar_limit() {
603 let _guard = registry_guard();
604 registry::reset_for_tests();
605 let path = unique_path("fgets_limit");
606 test_support::fs::write(&path, "abcdefghij\nrest\n").unwrap();
607 let handle = fopen_path(&path);
608
609 let eval =
610 run_evaluate(&Value::Num(handle.fid as f64), &[Value::Num(5.0)]).expect("limited read");
611 match eval.first_output() {
612 Value::CharArray(ca) => {
613 let text: String = ca.data.iter().collect();
614 assert_eq!(text, "abcd");
615 }
616 other => panic!("expected char array, got {other:?}"),
617 }
618 match &eval.outputs()[1] {
619 Value::Tensor(t) => {
620 assert!(t.data.is_empty());
621 }
622 other => panic!("expected empty numeric tensor, got {other:?}"),
623 }
624
625 test_support::fs::remove_file(&path).unwrap();
626 }
627
628 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
629 #[test]
630 fn fgets_errors_for_write_only_identifier() {
631 let _guard = registry_guard();
632 registry::reset_for_tests();
633 let path = unique_path("fgets_write_only");
634 test_support::fs::write(&path, "payload").unwrap();
635 let eval = run_fopen(&[
636 Value::from(path.to_string_lossy().to_string()),
637 Value::from("w"),
638 ])
639 .expect("fopen");
640 let open = eval.as_open().expect("open outputs");
641 assert!(open.fid >= 3.0);
642 let err = unwrap_error_message(run_evaluate(&Value::Num(open.fid), &[]).unwrap_err());
643 assert_eq!(err, "fgets: file identifier is not open for reading");
644 test_support::fs::remove_file(&path).unwrap();
645 }
646
647 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
648 #[test]
649 fn fgets_respects_limit_before_crlf_sequence() {
650 let _guard = registry_guard();
651 registry::reset_for_tests();
652 let path = unique_path("fgets_limit_crlf");
653 test_support::fs::write(&path, b"ABCDE\r\nnext\n").unwrap();
654 let handle = fopen_path(&path);
655
656 let first =
657 run_evaluate(&Value::Num(handle.fid as f64), &[Value::Num(3.0)]).expect("first");
658 match first.first_output() {
659 Value::CharArray(ca) => {
660 let text: String = ca.data.iter().collect();
661 assert_eq!(text, "AB");
662 }
663 other => panic!("expected char array, got {other:?}"),
664 }
665 match &first.outputs()[1] {
666 Value::Tensor(t) => assert!(t.data.is_empty()),
667 other => panic!("expected empty numeric tensor, got {other:?}"),
668 }
669
670 let second = run_evaluate(&Value::Num(handle.fid as f64), &[]).expect("second");
671 match second.first_output() {
672 Value::CharArray(ca) => {
673 let text: String = ca.data.iter().collect();
674 assert_eq!(text, "CDE\r\n");
675 }
676 other => panic!("expected char array, got {other:?}"),
677 }
678 match &second.outputs()[1] {
679 Value::Tensor(t) => assert_eq!(t.data, vec![13.0, 10.0]),
680 other => panic!("expected CRLF terminators, got {other:?}"),
681 }
682
683 let third = run_evaluate(&Value::Num(handle.fid as f64), &[]).expect("third");
684 match third.first_output() {
685 Value::CharArray(ca) => {
686 let text: String = ca.data.iter().collect();
687 assert_eq!(text, "next\n");
688 }
689 other => panic!("expected char array, got {other:?}"),
690 }
691
692 test_support::fs::remove_file(&path).unwrap();
693 }
694
695 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
696 #[test]
697 fn fgets_handles_crlf_newlines() {
698 let _guard = registry_guard();
699 registry::reset_for_tests();
700 let path = unique_path("fgets_crlf");
701 test_support::fs::write(&path, b"first line\r\nsecond\r\n").unwrap();
702 let handle = fopen_path(&path);
703
704 let eval = run_evaluate(&Value::Num(handle.fid as f64), &[]).expect("fgets");
705 let outputs = eval.outputs();
706 match &outputs[0] {
707 Value::CharArray(ca) => {
708 let text: String = ca.data.iter().collect();
709 assert_eq!(text, "first line\r\n");
710 }
711 other => panic!("expected char array, got {other:?}"),
712 }
713 match &outputs[1] {
714 Value::Tensor(t) => {
715 assert_eq!(t.data, vec![13.0, 10.0]);
716 }
717 other => panic!("expected numeric tensor, got {other:?}"),
718 }
719
720 test_support::fs::remove_file(&path).unwrap();
721 }
722
723 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
724 #[test]
725 fn fgets_decodes_latin1() {
726 let _guard = registry_guard();
727 registry::reset_for_tests();
728 let path = unique_path("fgets_latin1");
729 test_support::fs::write(&path, [0x48u8, 0x6f, 0x6c, 0x61, 0x20, 0xf1, b'\n']).unwrap();
730 let eval = run_fopen(&[
731 Value::from(path.to_string_lossy().to_string()),
732 Value::from("r"),
733 Value::from("native"),
734 Value::from("latin1"),
735 ])
736 .expect("fopen");
737 let open = eval.as_open().expect("open outputs");
738 let fid = open.fid as i32;
739
740 let read = run_evaluate(&Value::Num(fid as f64), &[]).expect("fgets");
741 match read.first_output() {
742 Value::CharArray(ca) => {
743 let text: String = ca.data.iter().collect();
744 assert_eq!(text, "Hola ñ\n");
745 }
746 other => panic!("expected char array, got {other:?}"),
747 }
748
749 let _ = registry::close(fid);
750 test_support::fs::remove_file(&path).unwrap();
751 }
752
753 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
754 #[test]
755 fn fgets_nchar_zero_returns_empty_char() {
756 let _guard = registry_guard();
757 registry::reset_for_tests();
758 let path = unique_path("fgets_zero");
759 test_support::fs::write(&path, "hello\n").unwrap();
760 let handle = fopen_path(&path);
761
762 let eval = run_evaluate(
763 &Value::Num(handle.fid as f64),
764 &[Value::Int(IntValue::I32(0))],
765 )
766 .expect("fgets");
767 match eval.first_output() {
768 Value::CharArray(ca) => {
769 assert_eq!(ca.rows, 1);
770 assert_eq!(ca.cols, 0);
771 assert!(ca.data.is_empty());
772 }
773 other => panic!("expected empty char array, got {other:?}"),
774 }
775
776 test_support::fs::remove_file(&path).unwrap();
777 }
778
779 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
780 #[test]
781 fn fgets_nchar_one_returns_empty_char() {
782 let _guard = registry_guard();
783 registry::reset_for_tests();
784 let path = unique_path("fgets_one");
785 test_support::fs::write(&path, "hello\n").unwrap();
786 let handle = fopen_path(&path);
787
788 let eval = run_evaluate(
789 &Value::Num(handle.fid as f64),
790 &[Value::Int(IntValue::I32(1))],
791 )
792 .expect("fgets");
793 match eval.first_output() {
794 Value::CharArray(ca) => {
795 assert_eq!(ca.rows, 1);
796 assert_eq!(ca.cols, 0);
797 assert!(ca.data.is_empty());
798 }
799 other => panic!("expected empty char array, got {other:?}"),
800 }
801
802 test_support::fs::remove_file(&path).unwrap();
803 }
804
805 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
806 #[test]
807 fn fgets_gathers_gpu_scalar_arguments() {
808 let _guard = registry_guard();
809 registry::reset_for_tests();
810 let path = unique_path("fgets_gpu_args");
811 test_support::fs::write(&path, b"abcdef\nextra").unwrap();
812 let handle = fopen_path(&path);
813
814 test_support::with_test_provider(|provider| {
815 let fid_host = [handle.fid as f64];
816 let fid_view = HostTensorView {
817 data: &fid_host,
818 shape: &[1, 1],
819 };
820 let fid_gpu = Value::GpuTensor(provider.upload(&fid_view).expect("upload fid"));
821
822 let limit_host = [3.0f64];
823 let limit_view = HostTensorView {
824 data: &limit_host,
825 shape: &[1, 1],
826 };
827 let limit_gpu = Value::GpuTensor(provider.upload(&limit_view).expect("upload limit"));
828
829 let eval = run_evaluate(&fid_gpu, &[limit_gpu]).expect("fgets");
830 match eval.first_output() {
831 Value::CharArray(ca) => {
832 let text: String = ca.data.iter().collect();
833 assert_eq!(text, "ab");
834 }
835 other => panic!("expected char array, got {other:?}"),
836 }
837 });
838
839 test_support::fs::remove_file(&path).unwrap();
840 }
841}