Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_text/tests/fmt.rs

30.1 KiB, 27 runs

created by r1870400018:11639, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Integration tests for the code formatter.
2//!
3//! Tests against representative Rust code patterns found in the
4//! fe2o3 codebase. Each test checks that the formatter produces
5//! reasonable output and that formatting is idempotent.
6
7use oxedyne_fe2o3_text::fmt::{
8 format_rust,
9 detect_language_from_source,
10 spec::FormatSpec,
11};
12
13fn fe2o3_fmt(source: &str) -> String {
14 let spec = FormatSpec::fe2o3();
15 format_rust(source, &spec).expect("format failed")
16}
17
18/// Assert that formatting is idempotent: formatting twice gives
19/// the same result as formatting once.
20fn assert_idempotent(source: &str) {
21 let first = fe2o3_fmt(source);
22 let second = fe2o3_fmt(&first);
23 assert_eq!(
24 first, second,
25 "not idempotent!\n--- first ---\n{}\n--- second ---\n{}",
26 first, second,
27 );
28}
29
30/// Assert the output contains an expected substring.
31fn assert_contains(source: &str, expected: &str) {
32 let out = fe2o3_fmt(source);
33 assert!(
34 out.contains(expected),
35 "expected {:?} in output:\n{}",
36 expected, out,
37 );
38}
39
40// ── Basic constructs ─────────────────────────────────────────────
41
42#[test]
43fn test_empty_fn() {
44 let out = fe2o3_fmt("fn main() {}");
45 assert!(out.contains("fn main()"), "output: {}", out);
46 assert_idempotent("fn main() {}");
47}
48
49#[test]
50fn test_fn_with_return_type() {
51 let out = fe2o3_fmt("fn foo() -> bool { true }");
52 assert!(out.contains("-> bool"), "output: {}", out);
53 assert_idempotent("fn foo() -> bool { true }");
54}
55
56#[test]
57fn test_fn_with_params() {
58 let source = "fn foo(x: u32, y: u64) -> bool { true }";
59 let out = fe2o3_fmt(source);
60 assert!(out.contains("x: u32"), "output: {}", out);
61 assert!(out.contains("y: u64"), "output: {}", out);
62 assert_idempotent(source);
63}
64
65#[test]
66fn test_struct_definition() {
67 let source = "pub struct CalClock {\n date: CalendarDate,\n time: ClockTime,\n}";
68 let out = fe2o3_fmt(source);
69 assert!(out.contains("pub struct CalClock"), "output: {}", out);
70 assert!(out.contains("date: CalendarDate"), "output: {}", out);
71 assert_idempotent(source);
72}
73
74#[test]
75fn test_unit_struct() {
76 let source = "pub struct Rand;";
77 let out = fe2o3_fmt(source);
78 assert!(out.contains("pub struct Rand;"), "output: {}", out);
79 assert_idempotent(source);
80}
81
82#[test]
83fn test_enum_definition() {
84 let source = "pub enum NumberType {\n SignedInt,\n UnsignedInt,\n FloatingPoint,\n}";
85 let out = fe2o3_fmt(source);
86 assert!(out.contains("pub enum NumberType"), "output: {}", out);
87 assert!(out.contains("SignedInt"), "output: {}", out);
88 assert_idempotent(source);
89}
90
91// ── Imports ──────────────────────────────────────────────────────
92
93#[test]
94fn test_simple_use() {
95 let source = "use std::fmt;";
96 let out = fe2o3_fmt(source);
97 assert!(out.contains("use std::fmt;"), "output: {:?}", out);
98 assert_idempotent(source);
99}
100
101#[test]
102fn test_grouped_use() {
103 let source = "use crate::{\n prelude::*,\n byte::B32,\n};";
104 let out = fe2o3_fmt(source);
105 assert!(out.contains("prelude"), "output: {:?}", out);
106 assert!(out.contains("B32"), "output: {:?}", out);
107 assert_idempotent(source);
108}
109
110// ── Impl blocks ──────────────────────────────────────────────────
111
112#[test]
113fn test_impl_block() {
114 let source = r#"impl Foo {
115 fn bar(&self) -> usize { 42 }
116}"#;
117 let out = fe2o3_fmt(source);
118 assert!(out.contains("impl Foo"), "output: {:?}", out);
119 assert_idempotent(source);
120}
121
122#[test]
123fn test_impl_trait() {
124 let source = r#"impl fmt::Display for Foo {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 write!(f, "{}", self.0)
127 }
128}"#;
129 let out = fe2o3_fmt(source);
130 assert!(out.contains("impl fmt::Display for Foo"), "output: {:?}", out);
131 assert_idempotent(source);
132}
133
134// ── Comments ─────────────────────────────────────────────────────
135
136#[test]
137fn test_preserves_line_comments() {
138 let source = "// This is a comment.\nfn foo() {}";
139 let out = fe2o3_fmt(source);
140 assert!(out.contains("// This is a comment."), "output: {:?}", out);
141 assert_idempotent(source);
142}
143
144#[test]
145fn test_preserves_doc_comments() {
146 let source = "/// Documentation for foo.\nfn foo() {}";
147 let out = fe2o3_fmt(source);
148 assert!(out.contains("/// Documentation for foo."), "output: {:?}", out);
149 assert_idempotent(source);
150}
151
152// ── Attributes ───────────────────────────────────────────────────
153
154#[test]
155fn test_preserves_attributes() {
156 let source = "#[derive(Clone, Debug)]\npub struct Foo;";
157 let out = fe2o3_fmt(source);
158 assert!(out.contains("#[derive(Clone, Debug)]"), "output: {:?}", out);
159 assert_idempotent(source);
160}
161
162// ── Where clauses ────────────────────────────────────────────────
163
164#[test]
165fn test_fn_with_where() {
166 let source = r#"fn value<T>() -> T
167where
168 T: Clone,
169{
170 todo!()
171}"#;
172 let out = fe2o3_fmt(source);
173 assert!(out.contains("where"), "output: {:?}", out);
174 assert!(out.contains("T: Clone"), "output: {:?}", out);
175 assert_idempotent(source);
176}
177
178// ── Multiple items ───────────────────────────────────────────────
179
180#[test]
181fn test_multiple_items() {
182 let source = r#"use std::fmt;
183
184fn main() {}
185
186struct Foo;"#;
187 let out = fe2o3_fmt(source);
188 assert!(out.contains("use std::fmt"), "output: {:?}", out);
189 assert!(out.contains("fn main"), "output: {:?}", out);
190 assert!(out.contains("struct Foo"), "output: {:?}", out);
191 assert_idempotent(source);
192}
193
194// ── Fe2o3-style patterns ─────────────────────────────────────────
195
196#[test]
197fn test_fe2o3_error_macro() {
198 // The err! macro uses semicolons — should be preserved verbatim.
199 let source = r#"fn foo() {
200 return Err(err!("Invalid input: {}", x; Invalid, Input));
201}"#;
202 let out = fe2o3_fmt(source);
203 assert!(out.contains("err!"), "output: {:?}", out);
204 assert_idempotent(source);
205}
206
207#[test]
208fn test_fe2o3_res_macro() {
209 let source = r#"fn foo() {
210 let val = res!(some_function());
211}"#;
212 let out = fe2o3_fmt(source);
213 assert!(out.contains("res!"), "output: {:?}", out);
214 assert_idempotent(source);
215}
216
217// ── Idempotency on real-world patterns ───────────────────────────
218
219#[test]
220fn test_idempotent_trait_impl() {
221 assert_idempotent(r#"impl ParseId<1> for u8 {
222 fn parse_id(s: &str) -> Outcome<Self> {
223 Ok(res!(u8::from_str(s)))
224 }
225}"#);
226}
227
228#[test]
229fn test_idempotent_enum_with_values() {
230 assert_idempotent(r#"pub enum Encoding {
231 Unknown = 0,
232 Binary = 1,
233 UTF8 = 2,
234}"#);
235}
236
237#[test]
238fn test_idempotent_complex_fn() {
239 assert_idempotent(r#"pub fn byte_slices_equal(a: &[u8], b: &[u8]) -> Outcome<()> {
240 for (i, ai) in a.iter().enumerate() {
241 if *ai != b[i] {
242 return Err(err!("Mismatch detected"; Input, Mismatch));
243 }
244 }
245 Ok(())
246}"#);
247}
248
249// ── Format spec options ──────────────────────────────────────────
250
251// ── Oxedyne fn signature layout ──────────────────────────────────
252
253#[test]
254fn test_oxedyne_fn_signature_flat() {
255 // Short signature: everything on one line.
256 let out = fe2o3_fmt("fn foo(x: u32) -> bool { true }");
257 // Return type should be inline.
258 assert!(out.contains("fn foo(x: u32) -> bool"), "output: {:?}", out);
259 assert_idempotent("fn foo(x: u32) -> bool { true }");
260}
261
262#[test]
263fn test_oxedyne_fn_signature_broken() {
264 // Force params to break by using a narrow width.
265 let mut spec = FormatSpec::fe2o3();
266 spec.max_width = 30;
267 let source = "fn generate_random_string(len: usize, charset: &str) -> String { todo!() }";
268 let out = format_rust(source, &spec).expect("format failed");
269 println!("output:\n{}", out);
270 // Params should be vertical, return type on own line.
271 assert!(out.contains("fn generate_random_string("), "output: {:?}", out);
272 assert!(out.contains("len:"), "expected vertical params, output: {:?}", out);
273 assert!(out.contains("usize,"), "expected vertical params, output: {:?}", out);
274 assert!(out.contains("charset: &str"), "expected vertical params, output: {:?}", out);
275 // Type column should be aligned (len padded to match charset).
276 assert!(out.contains("len: usize,"), "expected aligned types, output: {:?}", out);
277 // Return type on own indented line (Oxedyne rule).
278 // The `-> String` should appear on a line by itself, indented.
279 let lines: Vec<&str> = out.lines().collect();
280 let ret_line = lines.iter().find(|l| l.contains("-> String"));
281 assert!(ret_line.is_some(), "expected '-> String' on its own line, output: {:?}", out);
282}
283
284#[test]
285fn test_config_from_str() {
286 let config = r#"
287# Oxedyne style with 2-space indent
288indent_width = 2
289max_width = 80
290where_indent = true
291fn_return_type = own_line
292brace_style = same_line_unless_where
293trailing_comma = true
294field_align_threshold = 30
295"#;
296 let spec = FormatSpec::from_config_str(config).expect("parse failed");
297 assert_eq!(spec.indent_width, 2);
298 assert_eq!(spec.max_width, 80);
299 assert_eq!(spec.where_indent, true);
300 assert_eq!(spec.field_align_threshold, 30);
301}
302
303#[test]
304fn test_config_unknown_key_is_error() {
305 let config = "nonexistent_option = true\n";
306 assert!(FormatSpec::from_config_str(config).is_err());
307}
308
309#[test]
310fn test_format_with_loaded_config() {
311 let config = "indent_width = 2\nmax_width = 100\n";
312 let spec = FormatSpec::from_config_str(config).expect("parse failed");
313 let source = "fn foo() {\n let x = 1;\n}";
314 let out = format_rust(source, &spec).expect("format failed");
315 assert!(out.contains("\n let"), "expected 2-space indent, got: {:?}", out);
316}
317
318#[test]
319fn test_spec_indent_width() {
320 let mut spec = FormatSpec::fe2o3();
321 spec.indent_width = 2;
322 let source = "fn foo() {\n let x = 1;\n}";
323 let out = format_rust(source, &spec).expect("format failed");
324 // Should use 2-space indent.
325 assert!(out.contains("\n let"), "expected 2-space indent, got: {:?}", out);
326}
327
328#[test]
329fn test_print_formatted_output() {
330 let source = r#"fn day_number(day: DayOfWeek) -> u8 {
331 match day {
332 Monday => 1,
333 Tuesday => 2,
334 Wednesday => 3,
335 Thursday => 4,
336 Friday => 5,
337 Saturday => 6,
338 Sunday => 7,
339 }
340}
341
342pub struct ImapServer {
343 pub store: u32,
344 pub users: u64,
345 pub hostname: String,
346}
347
348use crate::{
349 prelude::*,
350 byte::B32,
351};
352
353use std::cmp::PartialOrd;
354
355/// Sampling method for range generation.
356#[derive(Clone, Copy, Debug)]
357pub enum SamplingMethod {
358 Uniform,
359 GaussianClampedDerived,
360}
361
362pub struct Rand;
363
364impl Rand {
365 pub fn rand_u8() -> u8 {
366 42
367 }
368
369 pub fn in_range<T>(lower: T, upper: T) -> T
370 where
371 T: PartialOrd
372 {
373 lower
374 }
375}
376"#;
377 let out = fe2o3_fmt(source);
378 println!("=== FORMATTED OUTPUT ===");
379 println!("{}", out);
380 println!("========================");
381 assert_idempotent(source);
382}
383
384// ── Comprehensive Oxedyne-style test ─────────────────────────────
385
386#[test]
387fn test_oxedyne_comprehensive() {
388 let source = r#"use crate::{
389 prelude::*,
390 byte::B32,
391};
392
393use std::fmt;
394
395/// Combined date and clock time.
396pub struct CalClock {
397 date: CalendarDate,
398 time: ClockTime,
399 zone: CalClockZone,
400}
401
402impl CalClock {
403 pub fn year(&self) -> i32 { self.date.year() }
404
405 pub fn day_of_week(&self) -> DayOfWeek {
406 match self.date.weekday() {
407 0 => DayOfWeek::Monday,
408 1 => DayOfWeek::Tuesday,
409 2 => DayOfWeek::Wednesday,
410 3 => DayOfWeek::Thursday,
411 4 => DayOfWeek::Friday,
412 5 => DayOfWeek::Saturday,
413 _ => DayOfWeek::Sunday,
414 }
415 }
416}
417
418impl fmt::Display for CalClock {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 write!(f, "{}", self.date)
421 }
422}
423"#;
424 let out = fe2o3_fmt(source);
425
426 // Struct field alignment.
427 assert!(out.contains("date: CalendarDate"), "output: {:?}", out);
428 assert!(out.contains("zone: CalClockZone"), "output: {:?}", out);
429
430 // Short fn on one line.
431 assert!(out.contains("pub fn year(&self) -> i32 { self.date.year() }"),
432 "expected single-line fn, output: {:?}", out);
433
434 // Trait impl fn stays on one line.
435 assert!(out.contains("fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result"),
436 "expected single-line trait fn sig, output: {:?}", out);
437
438 // Match arm alignment.
439 assert!(out.contains("0 => DayOfWeek::Monday"), "output: {:?}", out);
440 assert!(out.contains("2 => DayOfWeek::Wednesday"), "output: {:?}", out);
441
442 // Idempotent.
443 assert_idempotent(source);
444}
445
446#[test]
447fn test_enum_discriminant_alignment() {
448 let source = r#"pub enum Encoding {
449 Unknown = 0,
450 Binary = 1,
451 UTF8 = 2,
452}"#;
453 let out = fe2o3_fmt(source);
454 // `=` column should be aligned.
455 assert!(out.contains("Unknown = 0"), "output: {:?}", out);
456 assert!(out.contains("Binary = 1"), "expected aligned =, output: {:?}", out);
457 assert!(out.contains("UTF8 = 2"), "expected aligned =, output: {:?}", out);
458 assert_idempotent(source);
459}
460
461#[test]
462fn test_method_chain_preserved() {
463 let source = r#"fn foo() {
464 let result = items.iter()
465 .filter(|x| x > 0)
466 .map(|x| x * 2)
467 .collect();
468}"#;
469 let out = fe2o3_fmt(source);
470 assert!(out.contains(".filter"), "output: {:?}", out);
471 assert!(out.contains(".map"), "output: {:?}", out);
472 assert!(out.contains(".collect"), "output: {:?}", out);
473 assert_idempotent(source);
474}
475
476#[test]
477fn test_method_chain_on_one_line() {
478 // Short chain on one line stays on one line.
479 let source = r#"fn foo() {
480 let x = items.iter().map(|x| x * 2).collect();
481}"#;
482 let out = fe2o3_fmt(source);
483 assert!(out.contains("items.iter().map"), "output: {:?}", out);
484 assert_idempotent(source);
485}
486
487#[test]
488fn test_nested_generics() {
489 let out = fe2o3_fmt("fn foo() -> Outcome<Vec<Token>> { todo!() }");
490 assert!(out.contains("Outcome<Vec<Token>>"), "expected no space in >>, output: {:?}", out);
491 assert_idempotent("fn foo() -> Outcome<Vec<Token>> { todo!() }");
492}
493
494#[test]
495fn test_binary_operator_continuation() {
496 let source = r#"fn foo() {
497 let x = a
498 && b
499 || c;
500}"#;
501 let out = fe2o3_fmt(source);
502 assert!(out.contains("&&"), "output: {:?}", out);
503 assert!(out.contains("||"), "output: {:?}", out);
504 assert_idempotent(source);
505}
506
507#[test]
508fn test_binop_precedence_indentation() {
509 // Test the format_binop_expr function directly.
510 use oxedyne_fe2o3_text::fmt::spec::FormatSpec;
511 let _spec = FormatSpec::fe2o3();
512 // The precedence-based indentation will be visible when the
513 // expression is formatted at a narrow width.
514 let source = r#"fn foo() {
515 let valid = name_len > 0 && age >= 18 && country == "NZ" || special;
516}"#;
517 let out = fe2o3_fmt(source);
518 assert!(out.contains("&&"), "output: {:?}", out);
519 assert!(out.contains("||"), "output: {:?}", out);
520 assert_idempotent(source);
521}
522
523#[test]
524fn test_binop_break_wide_expression() {
525 // Narrow width forces the expression to break. Oxedyne rule:
526 // break before each operator; each lower-precedence operator
527 // indents one level deeper than the previous.
528 let mut spec = FormatSpec::fe2o3();
529 spec.max_width = 40;
530 let source = "fn foo() {\n let valid = aaaa && bbbb && cccc || dddd;\n}\n";
531 let out = format_rust(source, &spec).expect("format failed");
532 // `&&` at +4 (tightest), `||` at +8 (one level deeper).
533 assert!(out.contains("\n && bbbb"), "expected break-before && at +4, output:\n{}", out);
534 assert!(out.contains("\n && cccc"), "expected break-before && at +4, output:\n{}", out);
535 assert!(out.contains("\n || dddd"), "expected break-before || at +8, output:\n{}", out);
536 // Idempotent.
537 let second = format_rust(&out, &spec).expect("format failed");
538 assert_eq!(out, second, "not idempotent:\n--- first ---\n{}\n--- second ---\n{}", out, second);
539}
540
541#[test]
542fn test_binop_stays_flat_when_fits() {
543 // At default width (100), the expression fits on one line.
544 let source = "fn foo() {\n let ok = a && b || c;\n}\n";
545 let out = fe2o3_fmt(source);
546 assert!(out.contains("a && b || c"), "should stay flat, output:\n{}", out);
547 assert_idempotent(source);
548}
549
550#[test]
551fn test_binop_return_statement() {
552 // Binary ops in a return statement.
553 let mut spec = FormatSpec::fe2o3();
554 spec.max_width = 30;
555 let source = "fn check() {\n return alpha && beta || gamma;\n}\n";
556 let out = format_rust(source, &spec).expect("format failed");
557 assert!(out.contains("return alpha"), "output:\n{}", out);
558 assert!(out.contains("\n && beta"), "expected break-before &&, output:\n{}", out);
559 assert!(out.contains("\n || gamma"), "expected break-before ||, output:\n{}", out);
560 let second = format_rust(&out, &spec).expect("format failed");
561 assert_eq!(out, second, "not idempotent:\n{}\nvs:\n{}", out, second);
562}
563
564#[test]
565fn test_binop_let_mut() {
566 // `let mut` should not confuse the handler.
567 let source = "fn foo() {\n let mut x = a && b;\n}\n";
568 let out = fe2o3_fmt(source);
569 assert!(out.contains("let mut x"), "output:\n{}", out);
570 assert!(out.contains("a && b"), "output:\n{}", out);
571 assert_idempotent(source);
572}
573
574#[test]
575fn test_return_no_expr() {
576 // `return;` should not produce `return ;`.
577 let source = "fn foo() {\n return;\n}\n";
578 let out = fe2o3_fmt(source);
579 assert!(out.contains("return;"), "expected 'return;' not 'return ;', output:\n{}", out);
580 assert_idempotent(source);
581}
582
583#[test]
584fn test_import_reorder() {
585 let mut spec = FormatSpec::fe2o3();
586 spec.import_reorder = true;
587 let source = "use crate::{\n zebra,\n alpha,\n middle,\n};";
588 let out = format_rust(source, &spec).expect("format failed");
589 // Items should be sorted alphabetically.
590 let alpha_pos = out.find("alpha").unwrap_or(999);
591 let middle_pos = out.find("middle").unwrap_or(999);
592 let zebra_pos = out.find("zebra").unwrap_or(999);
593 assert!(alpha_pos < middle_pos, "alpha should be before middle, output: {:?}", out);
594 assert!(middle_pos < zebra_pos, "middle should be before zebra, output: {:?}", out);
595}
596
597// ── Full fe2o3-style integration test ────────────────────────────
598
599#[test]
600fn test_fe2o3_realistic_code() {
601 let source = r#"use crate::{
602 prelude::*,
603 byte::B32,
604};
605
606use std::{
607 fmt,
608 sync::Arc,
609};
610
611
612/// Broad token categories.
613#[derive(Clone, Debug, Eq, PartialEq)]
614pub enum TokenKind {
615 Keyword,
616 Ident,
617 Number,
618 StringLit,
619 Eof,
620}
621
622/// Byte range in the source text.
623#[derive(Clone, Copy, Debug, Default)]
624pub struct Span {
625 pub start: usize,
626 pub end: usize,
627}
628
629#[repr(u8)]
630pub enum Encoding {
631 Unknown = 0,
632 Binary = 1,
633 UTF8 = 2,
634}
635
636pub struct Lexer {
637 src: String,
638 pos: usize,
639 tokens: Vec<Token>,
640}
641
642impl Lexer {
643 pub fn new(src: String) -> Self {
644 Self { src, pos: 0, tokens: Vec::new() }
645 }
646
647 pub fn peek(&self) -> &Token { &self.tokens[self.pos] }
648
649 pub fn at_eof(&self) -> bool {
650 matches!(self.peek().kind, TokenKind::Eof)
651 }
652
653 pub fn lex_all(
654 src: &str,
655 max_tokens: usize,
656 strict: bool,
657 )
658 -> Outcome<Vec<Token>>
659 {
660 let mut tokens = Vec::new();
661 for (i, ch) in src.chars().enumerate() {
662 if i >= max_tokens {
663 break;
664 }
665 match ch {
666 '0'..='9' => tokens.push(Token { kind: TokenKind::Number }),
667 'a'..='z' => tokens.push(Token { kind: TokenKind::Ident }),
668 _ => tokens.push(Token { kind: TokenKind::Eof }),
669 }
670 }
671 Ok(tokens)
672 }
673}
674
675impl fmt::Display for TokenKind {
676 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
677 write!(f, "{:?}", self)
678 }
679}
680"#;
681 let out = fe2o3_fmt(source);
682 println!("=== REALISTIC OUTPUT ===");
683 println!("{}", out);
684 println!("========================");
685
686 // Struct field alignment.
687 assert!(out.contains("start: usize"), "output: {:?}", out);
688 assert!(out.contains("end: usize"), "expected aligned fields, output: {:?}", out);
689
690 // Enum discriminant alignment.
691 assert!(out.contains("Binary = 1"), "expected aligned =, output: {:?}", out);
692
693 // Match arm alignment.
694 assert!(out.contains("'0'..='9' =>"), "output: {:?}", out);
695 assert!(out.contains("'a'..='z' =>"), "output: {:?}", out);
696
697 // Short fns on one line.
698 assert!(out.contains("pub fn at_eof(&self) -> bool"),
699 "expected single-line fn, output: {:?}", out);
700
701 // Trait impl Display stays on one line.
702 assert!(out.contains("fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result"),
703 "output: {:?}", out);
704
705 // Idempotent.
706 assert_idempotent(source);
707}
708
709// ── Workspace regression ────────────────────────────────────────
710
711/// Run the formatter against every `.rs` file in the fe2o3 workspace.
712///
713/// Each file is processed independently with no state carried
714/// across files. Checks that the formatter does not crash and
715/// that the output is idempotent (formatting twice gives the
716/// same result as formatting once).
717/// Whether an error is the formatter declining a file rather than
718/// dropping one of its comments.
719fn is_comment_refusal(e: &oxedyne_fe2o3_core::error::Error<oxedyne_fe2o3_core::error::ErrTag>) -> bool {
720 format!("{}", e).contains("would alter its comments")
721}
722
723#[test]
724fn test_workspace_regression() {
725 use std::fs;
726 use std::path::PathBuf;
727
728 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf();
729 let spec = FormatSpec::fe2o3();
730
731 let mut checked = 0usize;
732 let mut failed: Vec<String> = Vec::new();
733 let mut refused = 0usize;
734
735 // Walk the workspace, skipping target/.
736 let mut dirs = vec![root.clone()];
737 while let Some(dir) = dirs.pop() {
738 let entries = match fs::read_dir(&dir) {
739 Ok(e) => e,
740 Err(_) => continue,
741 };
742 for entry in entries.flatten() {
743 let path = entry.path();
744 if path.is_dir() {
745 let name = path.file_name().unwrap_or_default();
746 // `.claude` holds agent worktrees: whole duplicate
747 // copies of this tree.
748 if name == "target" || name == ".git" || name == ".claude" {
749 continue;
750 }
751 dirs.push(path);
752 continue;
753 }
754 if path.extension().and_then(|e| e.to_str()) != Some("rs") {
755 continue;
756 }
757 let source = match fs::read_to_string(&path) {
758 Ok(s) => s,
759 Err(_) => continue,
760 };
761 if source.is_empty() {
762 continue;
763 }
764 let rel = path.strip_prefix(&root).unwrap_or(&path);
765
766 // Format once. A file the formatter declines because it
767 // could not carry every comment through is not a failure --
768 // declining is the safe outcome, and the point of the guard.
769 // Only an unexpected error, or non-idempotent output, is.
770 let first = match format_rust(&source, &spec) {
771 Ok(s) => s,
772 Err(e) => {
773 if is_comment_refusal(&e) {
774 refused += 1;
775 } else {
776 failed.push(format!("{}: format error: {}", rel.display(), e));
777 }
778 continue;
779 }
780 };
781
782 // Idempotency: format the output a second time.
783 let second = match format_rust(&first, &spec) {
784 Ok(s) => s,
785 Err(e) => {
786 if is_comment_refusal(&e) {
787 refused += 1;
788 } else {
789 failed.push(format!("{}: second-pass error: {}", rel.display(), e));
790 }
791 continue;
792 }
793 };
794 if first != second {
795 // Find the first divergent line for the report.
796 let fl: Vec<&str> = first.lines().collect();
797 let sl: Vec<&str> = second.lines().collect();
798 let mut diff_line = 0;
799 for (i, (a, b)) in fl.iter().zip(sl.iter()).enumerate() {
800 if a != b {
801 diff_line = i + 1;
802 break;
803 }
804 }
805 if diff_line == 0 {
806 diff_line = fl.len().min(sl.len()) + 1;
807 }
808 failed.push(format!(
809 "{}: not idempotent (first divergence at line {})",
810 rel.display(), diff_line,
811 ));
812 }
813
814 checked += 1;
815 }
816 }
817
818 println!(
819 "Checked {} files, {} declined for comment safety, {} failures.",
820 checked, refused, failed.len(),
821 );
822 if !failed.is_empty() {
823 for f in &failed {
824 println!(" FAIL: {}", f);
825 }
826 panic!("{} file(s) failed the regression check.", failed.len());
827 }
828}
829
830// ── Content-based language detection ─────────────────────────────
831
832#[test]
833fn test_detect_rust_from_source() {
834 let src = r#"
835use std::collections::HashMap;
836
837pub fn main() {
838 let mut map = HashMap::new();
839 map.insert("key", "value");
840}
841"#;
842 assert_eq!(detect_language_from_source(src), Some("rust"));
843}
844
845#[test]
846fn test_detect_go_from_source() {
847 let src = r#"
848package main
849
850import "fmt"
851
852func main() {
853 x := 42
854 fmt.Println(x)
855}
856"#;
857 assert_eq!(detect_language_from_source(src), Some("go"));
858}
859
860#[test]
861fn test_detect_python_from_source() {
862 let src = r#"
863from collections import defaultdict
864
865class Foo:
866 def __init__(self):
867 self.data = defaultdict(list)
868
869 def add(self, key, value):
870 self.data[key].append(value)
871"#;
872 assert_eq!(detect_language_from_source(src), Some("python"));
873}
874
875#[test]
876fn test_detect_python_shebang() {
877 let src = "#!/usr/bin/env python3\nimport sys\nprint(sys.argv)\n";
878 assert_eq!(detect_language_from_source(src), Some("python"));
879}
880
881#[test]
882fn test_detect_js_from_source() {
883 let src = r#"
884import { readFile } from 'fs';
885
886const handler = async (req) => {
887 if (req.method === 'GET') {
888 console.log('request received');
889 }
890};
891
892export default handler;
893"#;
894 assert_eq!(detect_language_from_source(src), Some("js"));
895}
896
897#[test]
898fn test_detect_java_from_source() {
899 let src = r#"
900import java.util.HashMap;
901
902public class Main {
903 public static void main(String[] args) {
904 System.out.println("hello");
905 }
906}
907"#;
908 assert_eq!(detect_language_from_source(src), Some("java"));
909}
910
911#[test]
912fn test_detect_c_from_source() {
913 let src = r#"
914#include <stdio.h>
915#include <stdlib.h>
916
917int main(int argc, char *argv[]) {
918 printf("hello %s\n", argv[0]);
919 return 0;
920}
921"#;
922 assert_eq!(detect_language_from_source(src), Some("c"));
923}
924
925#[test]
926fn test_detect_none_for_empty() {
927 assert_eq!(detect_language_from_source(""), None);
928 assert_eq!(detect_language_from_source("hello world"), None);
929}
930
931// ── Corpus-based detection tests ─────────────────────────────────
932
933/// Walk a corpus directory and verify detection for every file.
934///
935/// The corpus lives in `tests/detect_corpus/`. Subdirectories are
936/// named after the expected language (`rust`, `c`, `go`, `java`,
937/// `js`, `python`) or `none` for non-code files. Each `.txt` file
938/// in a language directory should be detected as that language.
939/// Files in `none/` should return `None`.
940#[test]
941fn test_detect_corpus() {
942 let corpus_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
943 .join("tests")
944 .join("detect_corpus");
945
946 let mut checked = 0;
947 let mut failed: Vec<String> = Vec::new();
948
949 let lang_dirs = ["rust", "c", "cpp", "csharp", "go", "java", "js", "python", "none"];
950
951 for lang_dir in &lang_dirs {
952 let dir = corpus_dir.join(lang_dir);
953 if !dir.exists() {
954 panic!("corpus directory missing: {}", dir.display());
955 }
956
957 let expected: Option<&str> = match *lang_dir {
958 "none" => None,
959 other => Some(other),
960 };
961
962 let mut entries: Vec<_> = std::fs::read_dir(&dir)
963 .expect("cannot read corpus dir")
964 .filter_map(|e| e.ok())
965 .filter(|e| e.path().extension().map_or(false, |ext| ext == "txt"))
966 .collect();
967 entries.sort_by_key(|e| e.path());
968
969 for entry in entries {
970 let path = entry.path();
971 let source = std::fs::read_to_string(&path)
972 .expect("cannot read corpus file");
973
974 let detected = detect_language_from_source(&source);
975 let rel = format!("{}/{}", lang_dir, path.file_name().unwrap().to_string_lossy());
976
977 if detected != expected {
978 failed.push(format!(
979 "{}: expected {:?}, got {:?}",
980 rel, expected, detected,
981 ));
982 }
983 checked += 1;
984 }
985 }
986
987 println!("Corpus: checked {} files, {} failures.", checked, failed.len());
988 if !failed.is_empty() {
989 for f in &failed {
990 println!(" FAIL: {}", f);
991 }
992 panic!("{} corpus file(s) failed detection.", failed.len());
993 }
994}