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 | |
| 7 | use oxedyne_fe2o3_text::fmt::{ |
| 8 | format_rust, |
| 9 | detect_language_from_source, |
| 10 | spec::FormatSpec, |
| 11 | }; |
| 12 | |
| 13 | fn 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. |
| 20 | fn 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. |
| 31 | fn 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] |
| 43 | fn 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] |
| 50 | fn 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] |
| 57 | fn 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] |
| 66 | fn 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] |
| 75 | fn 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] |
| 83 | fn 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] |
| 94 | fn 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] |
| 102 | fn 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] |
| 113 | fn 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] |
| 123 | fn 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] |
| 137 | fn 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] |
| 145 | fn 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] |
| 155 | fn 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] |
| 165 | fn test_fn_with_where() { |
| 166 | let source = r#"fn value<T>() -> T |
| 167 | where |
| 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] |
| 181 | fn test_multiple_items() { |
| 182 | let source = r#"use std::fmt; |
| 183 | |
| 184 | fn main() {} |
| 185 | |
| 186 | struct 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] |
| 197 | fn 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] |
| 208 | fn 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] |
| 220 | fn 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] |
| 229 | fn 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] |
| 238 | fn 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] |
| 254 | fn 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] |
| 263 | fn 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] |
| 285 | fn test_config_from_str() { |
| 286 | let config = r#" |
| 287 | # Oxedyne style with 2-space indent |
| 288 | indent_width = 2 |
| 289 | max_width = 80 |
| 290 | where_indent = true |
| 291 | fn_return_type = own_line |
| 292 | brace_style = same_line_unless_where |
| 293 | trailing_comma = true |
| 294 | field_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] |
| 304 | fn 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] |
| 310 | fn 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] |
| 319 | fn 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] |
| 329 | fn 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 | |
| 342 | pub struct ImapServer { |
| 343 | pub store: u32, |
| 344 | pub users: u64, |
| 345 | pub hostname: String, |
| 346 | } |
| 347 | |
| 348 | use crate::{ |
| 349 | prelude::*, |
| 350 | byte::B32, |
| 351 | }; |
| 352 | |
| 353 | use std::cmp::PartialOrd; |
| 354 | |
| 355 | /// Sampling method for range generation. |
| 356 | #[derive(Clone, Copy, Debug)] |
| 357 | pub enum SamplingMethod { |
| 358 | Uniform, |
| 359 | GaussianClampedDerived, |
| 360 | } |
| 361 | |
| 362 | pub struct Rand; |
| 363 | |
| 364 | impl 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] |
| 387 | fn test_oxedyne_comprehensive() { |
| 388 | let source = r#"use crate::{ |
| 389 | prelude::*, |
| 390 | byte::B32, |
| 391 | }; |
| 392 | |
| 393 | use std::fmt; |
| 394 | |
| 395 | /// Combined date and clock time. |
| 396 | pub struct CalClock { |
| 397 | date: CalendarDate, |
| 398 | time: ClockTime, |
| 399 | zone: CalClockZone, |
| 400 | } |
| 401 | |
| 402 | impl 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 | |
| 418 | impl 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] |
| 447 | fn 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] |
| 462 | fn 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] |
| 477 | fn 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] |
| 488 | fn 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] |
| 495 | fn 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] |
| 508 | fn 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] |
| 524 | fn 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] |
| 542 | fn 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] |
| 551 | fn 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] |
| 565 | fn 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] |
| 575 | fn 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] |
| 584 | fn 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] |
| 600 | fn test_fe2o3_realistic_code() { |
| 601 | let source = r#"use crate::{ |
| 602 | prelude::*, |
| 603 | byte::B32, |
| 604 | }; |
| 605 | |
| 606 | use std::{ |
| 607 | fmt, |
| 608 | sync::Arc, |
| 609 | }; |
| 610 | |
| 611 | |
| 612 | /// Broad token categories. |
| 613 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 614 | pub 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)] |
| 624 | pub struct Span { |
| 625 | pub start: usize, |
| 626 | pub end: usize, |
| 627 | } |
| 628 | |
| 629 | #[repr(u8)] |
| 630 | pub enum Encoding { |
| 631 | Unknown = 0, |
| 632 | Binary = 1, |
| 633 | UTF8 = 2, |
| 634 | } |
| 635 | |
| 636 | pub struct Lexer { |
| 637 | src: String, |
| 638 | pos: usize, |
| 639 | tokens: Vec<Token>, |
| 640 | } |
| 641 | |
| 642 | impl 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 | |
| 675 | impl 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. |
| 719 | fn 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] |
| 724 | fn 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] |
| 833 | fn test_detect_rust_from_source() { |
| 834 | let src = r#" |
| 835 | use std::collections::HashMap; |
| 836 | |
| 837 | pub 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] |
| 846 | fn test_detect_go_from_source() { |
| 847 | let src = r#" |
| 848 | package main |
| 849 | |
| 850 | import "fmt" |
| 851 | |
| 852 | func main() { |
| 853 | x := 42 |
| 854 | fmt.Println(x) |
| 855 | } |
| 856 | "#; |
| 857 | assert_eq!(detect_language_from_source(src), Some("go")); |
| 858 | } |
| 859 | |
| 860 | #[test] |
| 861 | fn test_detect_python_from_source() { |
| 862 | let src = r#" |
| 863 | from collections import defaultdict |
| 864 | |
| 865 | class 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] |
| 876 | fn 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] |
| 882 | fn test_detect_js_from_source() { |
| 883 | let src = r#" |
| 884 | import { readFile } from 'fs'; |
| 885 | |
| 886 | const handler = async (req) => { |
| 887 | if (req.method === 'GET') { |
| 888 | console.log('request received'); |
| 889 | } |
| 890 | }; |
| 891 | |
| 892 | export default handler; |
| 893 | "#; |
| 894 | assert_eq!(detect_language_from_source(src), Some("js")); |
| 895 | } |
| 896 | |
| 897 | #[test] |
| 898 | fn test_detect_java_from_source() { |
| 899 | let src = r#" |
| 900 | import java.util.HashMap; |
| 901 | |
| 902 | public 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] |
| 912 | fn test_detect_c_from_source() { |
| 913 | let src = r#" |
| 914 | #include <stdio.h> |
| 915 | #include <stdlib.h> |
| 916 | |
| 917 | int 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] |
| 926 | fn 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] |
| 941 | fn 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 | } |