1use std::io::Write;
4use std::path::{Path, PathBuf};
5
6use runmat_builtins::{CharArray, IntValue, LogicalArray, StringArray, 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::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
14use runmat_filesystem::OpenOptions;
15
16#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::filetext::filewrite")]
17pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
18 name: "filewrite",
19 op_kind: GpuOpKind::Custom("io-file-write"),
20 supported_precisions: &[],
21 broadcast: BroadcastSemantics::None,
22 provider_hooks: &[],
23 constant_strategy: ConstantStrategy::InlineLiteral,
24 residency: ResidencyPolicy::GatherImmediately,
25 nan_mode: ReductionNaN::Include,
26 two_pass_threshold: None,
27 workgroup_size: None,
28 accepts_nan_mode: false,
29 notes: "Performs synchronous host file I/O; GPU providers do not participate.",
30};
31
32#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::filetext::filewrite")]
33pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
34 name: "filewrite",
35 shape: ShapeRequirements::Any,
36 constant_strategy: ConstantStrategy::InlineLiteral,
37 elementwise: None,
38 reduction: None,
39 emits_nan: false,
40 notes: "Standalone host-side operation; never fused with other kernels.",
41};
42
43const BUILTIN_NAME: &str = "filewrite";
44
45fn filewrite_error(message: impl Into<String>) -> RuntimeError {
46 build_runtime_error(message)
47 .with_builtin(BUILTIN_NAME)
48 .build()
49}
50
51fn map_control_flow(err: RuntimeError) -> RuntimeError {
52 let identifier = err.identifier().map(str::to_string);
53 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
54 .with_builtin(BUILTIN_NAME)
55 .with_source(err);
56 if let Some(identifier) = identifier {
57 builder = builder.with_identifier(identifier);
58 }
59 builder.build()
60}
61
62fn map_control_flow_with_context(err: RuntimeError, context: &str) -> RuntimeError {
63 let identifier = err.identifier().map(str::to_string);
64 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {context}: {}", err.message()))
65 .with_builtin(BUILTIN_NAME)
66 .with_source(err);
67 if let Some(identifier) = identifier {
68 builder = builder.with_identifier(identifier);
69 }
70 builder.build()
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74enum FileEncoding {
75 Auto,
76 Utf8,
77 Ascii,
78 Latin1,
79 Raw,
80}
81
82impl FileEncoding {
83 fn from_label(label: &str) -> Option<Self> {
84 match label.trim().to_ascii_lowercase().as_str() {
85 "auto" | "default" | "system" | "native" => Some(FileEncoding::Auto),
86 "utf-8" | "utf8" | "utf_8" | "unicode" => Some(FileEncoding::Utf8),
87 "ascii" | "us-ascii" | "us_ascii" | "usascii" => Some(FileEncoding::Ascii),
88 "latin1" | "latin-1" | "latin_1" | "iso-8859-1" | "iso8859-1" | "iso88591" => {
89 Some(FileEncoding::Latin1)
90 }
91 "raw" | "bytes" | "byte" | "binary" => Some(FileEncoding::Raw),
92 _ => None,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum WriteMode {
99 Overwrite,
100 Append,
101}
102
103impl WriteMode {
104 fn from_label(label: &str) -> Option<Self> {
105 match label.trim().to_ascii_lowercase().as_str() {
106 "overwrite" | "replace" | "truncate" => Some(WriteMode::Overwrite),
107 "append" | "add" => Some(WriteMode::Append),
108 _ => None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy)]
114struct FilewriteOptions {
115 encoding: FileEncoding,
116 write_mode: WriteMode,
117}
118
119impl Default for FilewriteOptions {
120 fn default() -> Self {
121 Self {
122 encoding: FileEncoding::Auto,
123 write_mode: WriteMode::Overwrite,
124 }
125 }
126}
127
128#[runtime_builtin(
129 name = "filewrite",
130 category = "io/filetext",
131 summary = "Write text or raw bytes to a file.",
132 keywords = "filewrite,io,write file,text file,append,encoding",
133 accel = "cpu",
134 type_resolver(crate::builtins::io::type_resolvers::filewrite_type),
135 builtin_path = "crate::builtins::io::filetext::filewrite"
136)]
137async fn filewrite_builtin(
138 path: Value,
139 data: Value,
140 rest: Vec<Value>,
141) -> crate::BuiltinResult<Value> {
142 let path = gather_if_needed_async(&path)
143 .await
144 .map_err(map_control_flow)?;
145 let data = gather_if_needed_async(&data)
146 .await
147 .map_err(map_control_flow)?;
148 let rest = gather_values(&rest).await?;
149 let options = parse_options(&rest)?;
150 let resolved = resolve_path(&path)?;
151 let payload = prepare_payload(&data, options.encoding)?;
152 let written = write_bytes(&resolved, &payload, options.write_mode)?;
153 Ok(Value::Num(written as f64))
154}
155
156async fn gather_values(values: &[Value]) -> BuiltinResult<Vec<Value>> {
157 let mut out = Vec::with_capacity(values.len());
158 for value in values {
159 out.push(
160 gather_if_needed_async(value)
161 .await
162 .map_err(map_control_flow)?,
163 );
164 }
165 Ok(out)
166}
167
168fn parse_options(args: &[Value]) -> BuiltinResult<FilewriteOptions> {
169 if args.is_empty() {
170 return Ok(FilewriteOptions::default());
171 }
172
173 let mut options = FilewriteOptions::default();
174 let mut idx = 0usize;
175 let mut encoding_specified = false;
176 let mut write_mode_specified = false;
177
178 if !args.is_empty() && !is_keyword(&args[0])? {
179 match encoding_from_value(&args[0]) {
180 Ok(enc) => {
181 options.encoding = enc;
182 encoding_specified = true;
183 idx = 1;
184 }
185 Err(err) => {
186 if args.len() == 1 {
187 return Err(err);
188 }
189 }
190 }
191 }
192
193 if !(args.len() - idx).is_multiple_of(2) {
194 return Err(filewrite_error(
195 "filewrite: expected keyword/value argument pairs",
196 ));
197 }
198
199 while idx < args.len() {
200 let key = keyword_name(&args[idx])?;
201 let value = &args[idx + 1];
202 if key.eq_ignore_ascii_case("encoding") {
203 if encoding_specified {
204 return Err(filewrite_error("filewrite: duplicate 'Encoding' argument"));
205 }
206 options.encoding = encoding_from_value(value)?;
207 encoding_specified = true;
208 } else if key.eq_ignore_ascii_case("writemode") {
209 if write_mode_specified {
210 return Err(filewrite_error("filewrite: duplicate 'WriteMode' argument"));
211 }
212 options.write_mode = write_mode_from_value(value)?;
213 write_mode_specified = true;
214 } else {
215 return Err(filewrite_error(format!(
216 "filewrite: unrecognised option '{}'",
217 key
218 )));
219 }
220 idx += 2;
221 }
222
223 Ok(options)
224}
225
226fn is_keyword(value: &Value) -> BuiltinResult<bool> {
227 let text = keyword_name(value)?;
228 Ok(text.eq_ignore_ascii_case("encoding") || text.eq_ignore_ascii_case("writemode"))
229}
230
231fn keyword_name(value: &Value) -> BuiltinResult<String> {
232 match value {
233 Value::String(s) => Ok(s.clone()),
234 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
235 Value::CharArray(_) => Err(filewrite_error(
236 "filewrite: keyword names must be 1-by-N character vectors",
237 )),
238 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
239 Value::StringArray(_) => Err(filewrite_error(
240 "filewrite: keyword inputs must be scalar string arrays",
241 )),
242 other => Err(filewrite_error(format!(
243 "filewrite: expected keyword as string scalar or character vector, got {other:?}"
244 ))),
245 }
246}
247
248fn encoding_from_value(value: &Value) -> BuiltinResult<FileEncoding> {
249 let label = keyword_name(value)?;
250 match FileEncoding::from_label(&label) {
251 Some(enc) => Ok(enc),
252 None if label.trim().is_empty() => Err(filewrite_error(
253 "filewrite: encoding name must not be empty",
254 )),
255 None => Err(filewrite_error(format!(
256 "filewrite: unsupported encoding '{}'",
257 label
258 ))),
259 }
260}
261
262fn write_mode_from_value(value: &Value) -> BuiltinResult<WriteMode> {
263 let label = keyword_name(value)?;
264 match WriteMode::from_label(&label) {
265 Some(mode) => Ok(mode),
266 None if label.trim().is_empty() => {
267 Err(filewrite_error("filewrite: write mode must not be empty"))
268 }
269 None => Err(filewrite_error(format!(
270 "filewrite: unsupported write mode '{}'; use 'overwrite' or 'append'",
271 label
272 ))),
273 }
274}
275
276fn resolve_path(value: &Value) -> BuiltinResult<PathBuf> {
277 match value {
278 Value::String(s) => normalize_path(s),
279 Value::CharArray(ca) if ca.rows == 1 => {
280 let path: String = ca.data.iter().collect();
281 normalize_path(&path)
282 }
283 Value::CharArray(_) => Err(filewrite_error(
284 "filewrite: filename must be a 1-by-N character vector",
285 )),
286 Value::StringArray(sa) if sa.data.len() == 1 => normalize_path(&sa.data[0]),
287 Value::StringArray(_) => Err(filewrite_error(
288 "filewrite: string array filename inputs must be scalar",
289 )),
290 other => Err(filewrite_error(format!(
291 "filewrite: expected filename as string scalar or character vector, got {other:?}"
292 ))),
293 }
294}
295
296fn normalize_path(raw: &str) -> BuiltinResult<PathBuf> {
297 if raw.is_empty() {
298 return Err(filewrite_error("filewrite: filename must not be empty"));
299 }
300 Ok(Path::new(raw).to_path_buf())
301}
302
303enum Payload {
304 Text(Vec<char>),
305 Bytes(Vec<u8>),
306}
307
308fn prepare_payload(data: &Value, encoding: FileEncoding) -> BuiltinResult<Vec<u8>> {
309 let payload = extract_payload(data)?;
310 match payload {
311 Payload::Text(chars) => encode_text(chars, encoding),
312 Payload::Bytes(bytes) => encode_bytes(bytes, encoding),
313 }
314}
315
316fn extract_payload(data: &Value) -> BuiltinResult<Payload> {
317 match data {
318 Value::CharArray(ca) => Ok(Payload::Text(char_array_to_text(ca))),
319 Value::String(s) => Ok(Payload::Text(s.chars().collect())),
320 Value::StringArray(sa) => Ok(Payload::Text(string_array_to_text(sa))),
321 Value::Num(n) => {
322 let byte = float_to_byte(*n)
323 .map_err(|err| map_control_flow_with_context(err, "filewrite: numeric value"))?;
324 Ok(Payload::Bytes(vec![byte]))
325 }
326 Value::Int(i) => {
327 let byte = int_value_to_byte(i)
328 .map_err(|err| map_control_flow_with_context(err, "filewrite: integer value"))?;
329 Ok(Payload::Bytes(vec![byte]))
330 }
331 Value::Bool(flag) => Ok(Payload::Bytes(vec![if *flag { 1 } else { 0 }])),
332 Value::Tensor(t) => Ok(Payload::Bytes(tensor_to_bytes(t)?)),
333 Value::LogicalArray(la) => Ok(Payload::Bytes(logical_to_bytes(la))),
334 Value::Cell(_) => Err(filewrite_error(
335 "filewrite: cell arrays are not supported inputs",
336 )),
337 Value::GpuTensor(_) => Err(filewrite_error(
338 "filewrite: internal error: GPU tensor should be gathered",
339 )),
340 Value::Complex(_, _) | Value::ComplexTensor(_) => Err(filewrite_error(
341 "filewrite: complex data must be converted to text before writing",
342 )),
343 other => Err(filewrite_error(format!(
344 "filewrite: unsupported data type {other:?}; expected text or uint8-compatible array"
345 ))),
346 }
347}
348
349fn char_array_to_text(ca: &CharArray) -> Vec<char> {
350 if ca.rows <= 1 {
351 return ca.data.clone();
352 }
353 let mut out = Vec::with_capacity(ca.rows * (ca.cols + 1));
354 for row in 0..ca.rows {
355 for col in 0..ca.cols {
356 out.push(ca.data[row * ca.cols + col]);
357 }
358 if row + 1 < ca.rows {
359 out.push('\n');
360 }
361 }
362 out
363}
364
365fn string_array_to_text(sa: &StringArray) -> Vec<char> {
366 if sa.data.is_empty() {
367 return Vec::new();
368 }
369 let mut combined = String::new();
370 for (idx, entry) in sa.data.iter().enumerate() {
371 combined.push_str(entry);
372 if idx + 1 < sa.data.len() {
373 combined.push('\n');
374 }
375 }
376 combined.chars().collect()
377}
378
379fn tensor_to_bytes(tensor: &Tensor) -> BuiltinResult<Vec<u8>> {
380 let mut out = Vec::with_capacity(tensor.data.len());
381 for (idx, value) in tensor.data.iter().enumerate() {
382 match float_to_byte(*value) {
383 Ok(byte) => out.push(byte),
384 Err(msg) => {
385 return Err(filewrite_error(format!(
386 "filewrite: numeric element {} {msg}",
387 idx
388 )));
389 }
390 }
391 }
392 Ok(out)
393}
394
395fn logical_to_bytes(array: &LogicalArray) -> Vec<u8> {
396 array
397 .data
398 .iter()
399 .map(|value| if *value != 0 { 1 } else { 0 })
400 .collect()
401}
402
403fn float_to_byte(value: f64) -> BuiltinResult<u8> {
404 if !value.is_finite() {
405 return Err(filewrite_error(format!(
406 "value {value} is not finite; cannot write as raw byte"
407 )));
408 }
409 let rounded = value.round();
410 if (value - rounded).abs() > 1e-9 {
411 return Err(filewrite_error(format!(
412 "value {value} is not an integer in the range 0..255"
413 )));
414 }
415 let int = rounded as i128;
416 if !(0..=255).contains(&int) {
417 return Err(filewrite_error(format!(
418 "value {value} is not in the range 0..255"
419 )));
420 }
421 Ok(int as u8)
422}
423
424fn int_value_to_byte(value: &IntValue) -> BuiltinResult<u8> {
425 match value {
426 IntValue::I8(v) => signed_to_byte(*v as i64),
427 IntValue::I16(v) => signed_to_byte(*v as i64),
428 IntValue::I32(v) => signed_to_byte(*v as i64),
429 IntValue::I64(v) => signed_to_byte(*v),
430 IntValue::U8(v) => Ok(*v),
431 IntValue::U16(v) => unsigned_to_byte(*v as u64),
432 IntValue::U32(v) => unsigned_to_byte(*v as u64),
433 IntValue::U64(v) => unsigned_to_byte(*v),
434 }
435}
436
437fn signed_to_byte(value: i64) -> BuiltinResult<u8> {
438 if !(0..=255).contains(&value) {
439 return Err(filewrite_error(format!(
440 "value {value} is not in the range 0..255"
441 )));
442 }
443 Ok(value as u8)
444}
445
446fn unsigned_to_byte(value: u64) -> BuiltinResult<u8> {
447 if value > 255 {
448 return Err(filewrite_error(format!(
449 "value {value} is not in the range 0..255"
450 )));
451 }
452 Ok(value as u8)
453}
454
455fn encode_text(chars: Vec<char>, encoding: FileEncoding) -> BuiltinResult<Vec<u8>> {
456 match encoding {
457 FileEncoding::Auto | FileEncoding::Utf8 => {
458 Ok(chars.iter().collect::<String>().into_bytes())
459 }
460 FileEncoding::Ascii => encode_ascii_chars(&chars),
461 FileEncoding::Latin1 | FileEncoding::Raw => encode_latin_chars(&chars, encoding),
462 }
463}
464
465fn encode_ascii_chars(chars: &[char]) -> BuiltinResult<Vec<u8>> {
466 let mut out = Vec::with_capacity(chars.len());
467 for &ch in chars {
468 if ch as u32 > 0x7F {
469 return Err(filewrite_error(format!(
470 "filewrite: character '{}' (U+{:04X}) cannot be encoded as ASCII",
471 ch, ch as u32
472 )));
473 }
474 out.push(ch as u8);
475 }
476 Ok(out)
477}
478
479fn encode_latin_chars(chars: &[char], encoding: FileEncoding) -> BuiltinResult<Vec<u8>> {
480 let mut out = Vec::with_capacity(chars.len());
481 for &ch in chars {
482 if ch as u32 > 0xFF {
483 return Err(filewrite_error(format!(
484 "filewrite: character '{}' (U+{:04X}) cannot be encoded as {}",
485 ch,
486 ch as u32,
487 match encoding {
488 FileEncoding::Latin1 => "Latin-1",
489 FileEncoding::Raw => "raw bytes",
490 _ => unreachable!(),
491 }
492 )));
493 }
494 out.push(ch as u8);
495 }
496 Ok(out)
497}
498
499fn encode_bytes(bytes: Vec<u8>, encoding: FileEncoding) -> BuiltinResult<Vec<u8>> {
500 if matches!(encoding, FileEncoding::Ascii) {
501 for (idx, byte) in bytes.iter().enumerate() {
502 if *byte > 0x7F {
503 return Err(filewrite_error(format!(
504 "filewrite: byte 0x{byte:02X} at index {idx} cannot be encoded as ASCII"
505 )));
506 }
507 }
508 }
509 Ok(bytes)
510}
511
512fn write_bytes(path: &Path, payload: &[u8], mode: WriteMode) -> BuiltinResult<usize> {
513 let mut options = OpenOptions::new();
514 options.create(true);
515 match mode {
516 WriteMode::Overwrite => {
517 options.write(true).truncate(true);
518 }
519 WriteMode::Append => {
520 options.write(true).append(true);
521 }
522 }
523
524 let mut file = options.open(path).map_err(|err| {
525 build_runtime_error(format!(
526 "filewrite: unable to open '{}': {}",
527 path.display(),
528 err
529 ))
530 .with_builtin(BUILTIN_NAME)
531 .with_source(err)
532 .build()
533 })?;
534
535 file.write_all(payload).map_err(|err| {
536 build_runtime_error(format!(
537 "filewrite: unable to write to '{}': {}",
538 path.display(),
539 err
540 ))
541 .with_builtin(BUILTIN_NAME)
542 .with_source(err)
543 .build()
544 })?;
545
546 file.flush().map_err(|err| {
547 build_runtime_error(format!(
548 "filewrite: unable to flush '{}': {}",
549 path.display(),
550 err
551 ))
552 .with_builtin(BUILTIN_NAME)
553 .with_source(err)
554 .build()
555 })?;
556
557 Ok(payload.len())
558}
559
560#[cfg(test)]
561pub(crate) mod tests {
562 use super::*;
563 use crate::builtins::common::test_support;
564 use crate::RuntimeError;
565 use runmat_time::unix_timestamp_ms;
566 use std::io::Read;
567
568 fn unwrap_error_message(err: RuntimeError) -> String {
569 err.message().to_string()
570 }
571
572 fn run_filewrite(path: Value, data: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
573 futures::executor::block_on(filewrite_builtin(path, data, rest))
574 }
575
576 fn unique_path(prefix: &str) -> PathBuf {
577 let millis = unix_timestamp_ms();
578 let mut path = std::env::temp_dir();
579 path.push(format!("runmat_{prefix}_{}_{}", std::process::id(), millis));
580 path
581 }
582
583 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
584 #[test]
585 fn filewrite_writes_text_content() {
586 let path = unique_path("filewrite_text");
587 let contents = "RunMat filewrite\nLine two\n";
588
589 let result = run_filewrite(
590 Value::from(path.to_string_lossy().to_string()),
591 Value::from(contents),
592 Vec::new(),
593 )
594 .expect("filewrite");
595
596 match result {
597 Value::Num(n) => assert_eq!(n as usize, contents.len()),
598 other => panic!("expected numeric byte count, got {other:?}"),
599 }
600
601 let written = test_support::fs::read_to_string(&path).expect("read filewrite output");
602 assert_eq!(written, contents);
603
604 let _ = test_support::fs::remove_file(&path);
605 }
606
607 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
608 #[test]
609 fn filewrite_appends_when_requested() {
610 let path = unique_path("filewrite_append");
611 test_support::fs::write(&path, "first\n").expect("write baseline");
612
613 run_filewrite(
614 Value::from(path.to_string_lossy().to_string()),
615 Value::from("second\n"),
616 vec![Value::from("WriteMode"), Value::from("append")],
617 )
618 .expect("filewrite append");
619
620 let written = test_support::fs::read_to_string(&path).expect("read appended file");
621 assert_eq!(written, "first\nsecond\n");
622
623 let _ = test_support::fs::remove_file(&path);
624 }
625
626 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
627 #[test]
628 fn filewrite_errors_on_invalid_ascii() {
629 let path = unique_path("filewrite_ascii_error");
630 let err = unwrap_error_message(
631 run_filewrite(
632 Value::from(path.to_string_lossy().to_string()),
633 Value::from("café"),
634 vec![Value::from("Encoding"), Value::from("ascii")],
635 )
636 .unwrap_err(),
637 );
638 assert!(
639 err.contains("cannot be encoded as ASCII"),
640 "unexpected error message: {err}"
641 );
642 assert!(!path.exists());
643 }
644
645 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
646 #[test]
647 fn filewrite_writes_raw_bytes_from_tensor() {
648 let path = unique_path("filewrite_raw_bytes");
649 let tensor = Tensor::new(vec![0.0, 127.0, 255.0], vec![3, 1]).expect("tensor");
650 run_filewrite(
651 Value::from(path.to_string_lossy().to_string()),
652 Value::Tensor(tensor),
653 vec![Value::from("Encoding"), Value::from("raw")],
654 )
655 .expect("filewrite raw");
656
657 let mut bytes = Vec::new();
658 runmat_filesystem::File::open(&path)
659 .expect("open raw file")
660 .read_to_end(&mut bytes)
661 .expect("read raw file");
662 assert_eq!(bytes, vec![0u8, 127u8, 255u8]);
663
664 let _ = test_support::fs::remove_file(&path);
665 }
666
667 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
668 #[test]
669 fn filewrite_numeric_scalar_writes_byte() {
670 let path = unique_path("filewrite_numeric_scalar");
671 run_filewrite(
672 Value::from(path.to_string_lossy().to_string()),
673 Value::Num(65.0),
674 Vec::new(),
675 )
676 .expect("filewrite numeric scalar");
677
678 let bytes = test_support::fs::read(&path).expect("read numeric scalar file");
679 assert_eq!(bytes, vec![65u8]);
680
681 let _ = test_support::fs::remove_file(&path);
682 }
683
684 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
685 #[test]
686 fn filewrite_bool_scalar_writes_byte() {
687 let path = unique_path("filewrite_bool_scalar");
688 run_filewrite(
689 Value::from(path.to_string_lossy().to_string()),
690 Value::Bool(true),
691 Vec::new(),
692 )
693 .expect("filewrite bool scalar");
694
695 let bytes = test_support::fs::read(&path).expect("read bool scalar file");
696 assert_eq!(bytes, vec![1u8]);
697
698 let _ = test_support::fs::remove_file(&path);
699 }
700
701 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
702 #[test]
703 fn filewrite_writes_logical_array_bytes() {
704 let path = unique_path("filewrite_logical_array");
705 let logical = LogicalArray::new(vec![0, 1, 2], vec![3]).expect("logical array");
706 run_filewrite(
707 Value::from(path.to_string_lossy().to_string()),
708 Value::LogicalArray(logical),
709 Vec::new(),
710 )
711 .expect("filewrite logical array");
712
713 let bytes = test_support::fs::read(&path).expect("read logical array file");
714 assert_eq!(bytes, vec![0u8, 1u8, 1u8]);
715
716 let _ = test_support::fs::remove_file(&path);
717 }
718
719 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
720 #[test]
721 fn filewrite_errors_on_numeric_out_of_range() {
722 let path = unique_path("filewrite_out_of_range");
723 let err = unwrap_error_message(
724 run_filewrite(
725 Value::from(path.to_string_lossy().to_string()),
726 Value::Num(300.0),
727 Vec::new(),
728 )
729 .unwrap_err(),
730 );
731 assert!(
732 err.contains("range 0..255"),
733 "unexpected error message: {err}"
734 );
735 assert!(!path.exists());
736 }
737
738 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
739 #[test]
740 fn filewrite_errors_on_non_integer_numeric() {
741 let path = unique_path("filewrite_non_integer");
742 let err = unwrap_error_message(
743 run_filewrite(
744 Value::from(path.to_string_lossy().to_string()),
745 Value::Num(std::f64::consts::PI),
746 Vec::new(),
747 )
748 .unwrap_err(),
749 );
750 assert!(
751 err.contains("not an integer"),
752 "unexpected error message: {err}"
753 );
754 assert!(!path.exists());
755 }
756
757 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
758 #[test]
759 fn filewrite_rejects_ascii_numeric_bytes_above_range() {
760 let path = unique_path("filewrite_ascii_bytes");
761 let tensor = Tensor::new(vec![255.0], vec![1, 1]).expect("tensor");
762 let err = unwrap_error_message(
763 run_filewrite(
764 Value::from(path.to_string_lossy().to_string()),
765 Value::Tensor(tensor),
766 vec![Value::from("Encoding"), Value::from("ascii")],
767 )
768 .unwrap_err(),
769 );
770 assert!(
771 err.contains("cannot be encoded as ASCII"),
772 "unexpected error message: {err}"
773 );
774 assert!(!path.exists());
775 }
776
777 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
778 #[test]
779 fn filewrite_positional_encoding_argument() {
780 let path = unique_path("filewrite_positional_encoding");
781 run_filewrite(
782 Value::from(path.to_string_lossy().to_string()),
783 Value::from("Espa\u{00F1}a"),
784 vec![Value::from("latin1")],
785 )
786 .expect("filewrite positional encoding");
787
788 let bytes = test_support::fs::read(&path).expect("read latin1 file");
789 assert_eq!(bytes, b"Espa\xF1a");
790
791 let _ = test_support::fs::remove_file(&path);
792 }
793
794 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
795 #[test]
796 fn filewrite_utf8_encoding_allows_arbitrary_bytes() {
797 let path = unique_path("filewrite_utf8_numeric");
798 let tensor = Tensor::new(vec![0.0, 255.0], vec![2, 1]).expect("tensor");
799 run_filewrite(
800 Value::from(path.to_string_lossy().to_string()),
801 Value::Tensor(tensor),
802 vec![Value::from("Encoding"), Value::from("utf-8")],
803 )
804 .expect("filewrite utf8 numeric");
805
806 let bytes = test_support::fs::read(&path).expect("read utf8 numeric file");
807 assert_eq!(bytes, vec![0u8, 255u8]);
808
809 let _ = test_support::fs::remove_file(&path);
810 }
811
812 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
813 #[test]
814 fn filewrite_rejects_unknown_option() {
815 let path = unique_path("filewrite_unknown_option");
816 let err = unwrap_error_message(
817 run_filewrite(
818 Value::from(path.to_string_lossy().to_string()),
819 Value::from("data"),
820 vec![Value::from("Mode"), Value::from("append")],
821 )
822 .unwrap_err(),
823 );
824 assert!(
825 err.contains("unrecognised option"),
826 "unexpected error message: {err}"
827 );
828 assert!(!path.exists());
829 }
830
831 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
832 #[test]
833 fn filewrite_rejects_duplicate_encoding() {
834 let path = unique_path("filewrite_duplicate_encoding");
835 let err = unwrap_error_message(
836 run_filewrite(
837 Value::from(path.to_string_lossy().to_string()),
838 Value::from("data"),
839 vec![
840 Value::from("utf-8"),
841 Value::from("Encoding"),
842 Value::from("ascii"),
843 ],
844 )
845 .unwrap_err(),
846 );
847 assert!(
848 err.contains("duplicate 'Encoding'"),
849 "unexpected error message: {err}"
850 );
851 assert!(!path.exists());
852 }
853
854 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
855 #[test]
856 fn filewrite_rejects_duplicate_writemode() {
857 let path = unique_path("filewrite_duplicate_writemode");
858 let err = unwrap_error_message(
859 run_filewrite(
860 Value::from(path.to_string_lossy().to_string()),
861 Value::from("data"),
862 vec![
863 Value::from("WriteMode"),
864 Value::from("append"),
865 Value::from("WriteMode"),
866 Value::from("overwrite"),
867 ],
868 )
869 .unwrap_err(),
870 );
871 assert!(
872 err.contains("duplicate 'WriteMode'"),
873 "unexpected error message: {err}"
874 );
875 assert!(!path.exists());
876 }
877
878 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
879 #[test]
880 fn filewrite_rejects_invalid_writemode_value() {
881 let path = unique_path("filewrite_invalid_writemode");
882 let err = unwrap_error_message(
883 run_filewrite(
884 Value::from(path.to_string_lossy().to_string()),
885 Value::from("data"),
886 vec![Value::from("WriteMode"), Value::from("invalid")],
887 )
888 .unwrap_err(),
889 );
890 assert!(
891 err.contains("unsupported write mode"),
892 "unexpected error message: {err}"
893 );
894 assert!(!path.exists());
895 }
896
897 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
898 #[test]
899 fn filewrite_rejects_invalid_encoding_value() {
900 let path = unique_path("filewrite_invalid_encoding");
901 let err = unwrap_error_message(
902 run_filewrite(
903 Value::from(path.to_string_lossy().to_string()),
904 Value::from("data"),
905 vec![Value::from("Encoding"), Value::from("utf-32")],
906 )
907 .unwrap_err(),
908 );
909 assert!(
910 err.contains("unsupported encoding"),
911 "unexpected error message: {err}"
912 );
913 assert!(!path.exists());
914 }
915
916 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
917 #[test]
918 fn filewrite_accepts_char_array_filename() {
919 let path = unique_path("filewrite_char_path");
920 let path_str = path.to_string_lossy();
921 let chars: Vec<char> = path_str.chars().collect();
922 let char_array = CharArray::new(chars, 1, path_str.len()).expect("char array path");
923
924 run_filewrite(
925 Value::CharArray(char_array),
926 Value::from("hello"),
927 Vec::new(),
928 )
929 .expect("filewrite char path");
930
931 let written = test_support::fs::read_to_string(&path).expect("read char path file");
932 assert_eq!(written, "hello");
933
934 let _ = test_support::fs::remove_file(&path);
935 }
936
937 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
938 #[test]
939 fn filewrite_string_array_stores_newlines() {
940 let path = unique_path("filewrite_string_array");
941 let array = StringArray::new(vec!["a".into(), "b".into(), "c".into()], vec![3, 1])
942 .expect("string array");
943 run_filewrite(
944 Value::from(path.to_string_lossy().to_string()),
945 Value::StringArray(array),
946 Vec::new(),
947 )
948 .expect("filewrite string array");
949
950 let written = test_support::fs::read_to_string(&path).expect("read string array file");
951 assert_eq!(written, "a\nb\nc");
952
953 let _ = test_support::fs::remove_file(&path);
954 }
955}