1Code.require_file("../../test_helper.exs", __DIR__) 2 3defmodule Mix.Tasks.FormatTest do 4 use MixTest.Case 5 6 import ExUnit.CaptureIO 7 8 defmodule FormatWithDepsApp do 9 def project do 10 [ 11 app: :format_with_deps, 12 version: "0.1.0", 13 deps: [{:my_dep, "0.1.0", path: "deps/my_dep"}] 14 ] 15 end 16 end 17 18 test "formats the given files", context do 19 in_tmp(context.test, fn -> 20 File.write!("a.ex", """ 21 foo bar 22 """) 23 24 Mix.Tasks.Format.run(["a.ex"]) 25 26 assert File.read!("a.ex") == """ 27 foo(bar) 28 """ 29 end) 30 end 31 32 test "formats the given pattern", context do 33 in_tmp(context.test, fn -> 34 File.write!("a.ex", """ 35 foo bar 36 """) 37 38 Mix.Tasks.Format.run(["*.ex", "a.ex"]) 39 40 assert File.read!("a.ex") == """ 41 foo(bar) 42 """ 43 end) 44 end 45 46 test "is a no-op if the file is already formatted", context do 47 in_tmp(context.test, fn -> 48 File.write!("a.ex", """ 49 foo(bar) 50 """) 51 52 File.touch!("a.ex", {{2010, 1, 1}, {0, 0, 0}}) 53 Mix.Tasks.Format.run(["a.ex"]) 54 assert File.stat!("a.ex").mtime == {{2010, 1, 1}, {0, 0, 0}} 55 end) 56 end 57 58 test "does not write file to disk on dry-run", context do 59 in_tmp(context.test, fn -> 60 File.write!("a.ex", """ 61 foo bar 62 """) 63 64 Mix.Tasks.Format.run(["a.ex", "--dry-run"]) 65 66 assert File.read!("a.ex") == """ 67 foo bar 68 """ 69 end) 70 end 71 72 test "reads file from stdin and prints to stdout", context do 73 in_tmp(context.test, fn -> 74 File.write!("a.ex", """ 75 foo bar 76 """) 77 78 output = 79 capture_io("foo( )", fn -> 80 Mix.Tasks.Format.run(["a.ex", "-"]) 81 end) 82 83 assert output == """ 84 foo() 85 """ 86 87 assert File.read!("a.ex") == """ 88 foo(bar) 89 """ 90 end) 91 end 92 93 test "reads file from stdin and prints to stdout with formatter", context do 94 in_tmp(context.test, fn -> 95 File.write!(".formatter.exs", """ 96 [locals_without_parens: [foo: 1]] 97 """) 98 99 output = 100 capture_io("foo :bar", fn -> 101 Mix.Tasks.Format.run(["-"]) 102 end) 103 104 assert output == """ 105 foo :bar 106 """ 107 end) 108 end 109 110 test "checks if file is formatted with --check-formatted", context do 111 in_tmp(context.test, fn -> 112 File.write!("a.ex", """ 113 foo bar 114 """) 115 116 assert_raise Mix.Error, ~r"mix format failed due to --check-formatted", fn -> 117 Mix.Tasks.Format.run(["a.ex", "--check-formatted"]) 118 end 119 120 assert File.read!("a.ex") == """ 121 foo bar 122 """ 123 124 assert Mix.Tasks.Format.run(["a.ex"]) == :ok 125 assert Mix.Tasks.Format.run(["a.ex", "--check-formatted"]) == :ok 126 127 assert File.read!("a.ex") == """ 128 foo(bar) 129 """ 130 end) 131 end 132 133 test "checks if stdin is formatted with --check-formatted" do 134 assert_raise Mix.Error, ~r"mix format failed due to --check-formatted", fn -> 135 capture_io("foo( )", fn -> 136 Mix.Tasks.Format.run(["--check-formatted", "-"]) 137 end) 138 end 139 140 output = 141 capture_io("foo()\n", fn -> 142 Mix.Tasks.Format.run(["--check-formatted", "-"]) 143 end) 144 145 assert output == "" 146 end 147 148 test "uses inputs and configuration from .formatter.exs", context do 149 in_tmp(context.test, fn -> 150 File.write!(".formatter.exs", """ 151 [ 152 inputs: ["a.ex"], 153 locals_without_parens: [foo: 1] 154 ] 155 """) 156 157 File.write!("a.ex", """ 158 foo bar baz 159 """) 160 161 Mix.Tasks.Format.run([]) 162 163 assert File.read!("a.ex") == """ 164 foo bar(baz) 165 """ 166 end) 167 end 168 169 test "expands patterns in inputs from .formatter.exs", context do 170 in_tmp(context.test, fn -> 171 File.write!(".formatter.exs", """ 172 [ 173 inputs: ["{a,.b}.ex"] 174 ] 175 """) 176 177 File.write!("a.ex", """ 178 foo bar 179 """) 180 181 File.write!(".b.ex", """ 182 foo bar 183 """) 184 185 Mix.Tasks.Format.run([]) 186 187 assert File.read!("a.ex") == """ 188 foo(bar) 189 """ 190 191 assert File.read!(".b.ex") == """ 192 foo(bar) 193 """ 194 end) 195 end 196 197 defmodule Elixir.SigilWPlugin do 198 @behaviour Mix.Tasks.Format 199 200 def features(opts) do 201 assert opts[:from_formatter_exs] == :yes 202 [sigils: [:W]] 203 end 204 205 def format(contents, opts) do 206 assert opts[:from_formatter_exs] == :yes 207 assert opts[:sigil] == :W 208 assert opts[:modifiers] == 'abc' 209 contents |> String.split(~r/\s/) |> Enum.join("\n") 210 end 211 end 212 213 test "uses sigil plugins from .formatter.exs", context do 214 in_tmp(context.test, fn -> 215 File.write!(".formatter.exs", """ 216 [ 217 inputs: ["a.ex"], 218 plugins: [SigilWPlugin], 219 from_formatter_exs: :yes 220 ] 221 """) 222 223 File.write!("a.ex", """ 224 if true do 225 ~W''' 226 foo bar baz 227 '''abc 228 end 229 """) 230 231 Mix.Tasks.Format.run([]) 232 233 assert File.read!("a.ex") == """ 234 if true do 235 ~W''' 236 foo 237 bar 238 baz 239 '''abc 240 end 241 """ 242 end) 243 end 244 245 defmodule Elixir.ExtensionWPlugin do 246 @behaviour Mix.Tasks.Format 247 248 def features(opts) do 249 assert opts[:from_formatter_exs] == :yes 250 [extensions: ~w(.w)] 251 end 252 253 def format(contents, opts) do 254 assert opts[:from_formatter_exs] == :yes 255 assert opts[:extension] == ".w" 256 contents |> String.split(~r/\s/) |> Enum.join("\n") 257 end 258 end 259 260 test "uses extension plugins from .formatter.exs", context do 261 in_tmp(context.test, fn -> 262 File.write!(".formatter.exs", """ 263 [ 264 inputs: ["a.w"], 265 plugins: [ExtensionWPlugin], 266 from_formatter_exs: :yes 267 ] 268 """) 269 270 File.write!("a.w", """ 271 foo bar baz 272 """) 273 274 Mix.Tasks.Format.run([]) 275 276 assert File.read!("a.w") == """ 277 foo 278 bar 279 baz 280 """ 281 end) 282 end 283 284 test "uses inputs and configuration from --dot-formatter", context do 285 in_tmp(context.test, fn -> 286 File.write!("custom_formatter.exs", """ 287 [ 288 inputs: ["a.ex"], 289 locals_without_parens: [foo: 1] 290 ] 291 """) 292 293 File.write!("a.ex", """ 294 foo bar baz 295 """) 296 297 Mix.Tasks.Format.run(["--dot-formatter", "custom_formatter.exs"]) 298 299 assert File.read!("a.ex") == """ 300 foo bar(baz) 301 """ 302 end) 303 end 304 305 test "can read exported configuration from subdirectories", context do 306 in_tmp(context.test, fn -> 307 File.write!(".formatter.exs", """ 308 [subdirectories: ["lib"]] 309 """) 310 311 File.mkdir_p!("lib") 312 313 File.write!("lib/.formatter.exs", """ 314 [inputs: "a.ex", locals_without_parens: [my_fun: 2]] 315 """) 316 317 {formatter, formatter_opts} = Mix.Tasks.Format.formatter_for_file("lib/extra/a.ex") 318 assert Keyword.get(formatter_opts, :locals_without_parens) == [my_fun: 2] 319 assert formatter.("my_fun 1, 2") == "my_fun 1, 2\n" 320 321 File.write!("lib/a.ex", """ 322 my_fun :foo, :bar 323 other_fun :baz 324 """) 325 326 Mix.Tasks.Format.run([]) 327 328 assert File.read!("lib/a.ex") == """ 329 my_fun :foo, :bar 330 other_fun(:baz) 331 """ 332 333 Mix.Tasks.Format.run(["lib/a.ex"]) 334 335 assert File.read!("lib/a.ex") == """ 336 my_fun :foo, :bar 337 other_fun(:baz) 338 """ 339 340 # No caching without a project 341 manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter") 342 refute File.regular?(manifest_path) 343 344 # Caching with a project 345 Mix.Project.push(__MODULE__.FormatWithDepsApp) 346 Mix.Tasks.Format.run(["lib/a.ex"]) 347 manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter") 348 assert File.regular?(manifest_path) 349 350 # Let's check that the manifest gets updated if it's stale. 351 File.touch!(manifest_path, {{2010, 1, 1}, {0, 0, 0}}) 352 353 Mix.Tasks.Format.run(["lib/a.ex"]) 354 assert File.stat!(manifest_path).mtime > {{2010, 1, 1}, {0, 0, 0}} 355 end) 356 end 357 358 test "can read exported configuration from dependencies", context do 359 in_tmp(context.test, fn -> 360 Mix.Project.push(__MODULE__.FormatWithDepsApp) 361 362 File.write!(".formatter.exs", """ 363 [import_deps: [:my_dep]] 364 """) 365 366 File.write!("a.ex", """ 367 my_fun :foo, :bar 368 """) 369 370 File.mkdir_p!("deps/my_dep/") 371 372 File.write!("deps/my_dep/.formatter.exs", """ 373 [export: [locals_without_parens: [my_fun: 2]]] 374 """) 375 376 Mix.Tasks.Format.run(["a.ex"]) 377 378 assert File.read!("a.ex") == """ 379 my_fun :foo, :bar 380 """ 381 382 manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter") 383 assert File.regular?(manifest_path) 384 385 # Let's check that the manifest gets updated if it's stale. 386 File.touch!(manifest_path, {{2010, 1, 1}, {0, 0, 0}}) 387 388 {_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("a.ex") 389 assert [my_fun: 2] = Keyword.get(formatter_opts, :locals_without_parens) 390 391 Mix.Tasks.Format.run(["a.ex"]) 392 assert File.stat!(manifest_path).mtime > {{2010, 1, 1}, {0, 0, 0}} 393 end) 394 end 395 396 test "can read exported configuration from dependencies and subdirectories", context do 397 in_tmp(context.test, fn -> 398 Mix.Project.push(__MODULE__.FormatWithDepsApp) 399 400 File.mkdir_p!("deps/my_dep/") 401 402 File.write!("deps/my_dep/.formatter.exs", """ 403 [export: [locals_without_parens: [my_fun: 2]]] 404 """) 405 406 File.mkdir_p!("lib/sub") 407 File.mkdir_p!("lib/not_used_and_wont_raise") 408 409 File.write!(".formatter.exs", """ 410 [subdirectories: ["lib"]] 411 """) 412 413 File.write!("lib/.formatter.exs", """ 414 [subdirectories: ["*"]] 415 """) 416 417 File.write!("lib/sub/.formatter.exs", """ 418 [inputs: "a.ex", import_deps: [:my_dep]] 419 """) 420 421 File.write!("lib/sub/a.ex", """ 422 my_fun :foo, :bar 423 other_fun :baz 424 """) 425 426 Mix.Tasks.Format.run([]) 427 428 assert File.read!("lib/sub/a.ex") == """ 429 my_fun :foo, :bar 430 other_fun(:baz) 431 """ 432 433 Mix.Tasks.Format.run(["lib/sub/a.ex"]) 434 435 assert File.read!("lib/sub/a.ex") == """ 436 my_fun :foo, :bar 437 other_fun(:baz) 438 """ 439 440 # Update .formatter.exs, check that file is updated 441 File.write!("lib/sub/.formatter.exs", """ 442 [inputs: "a.ex"] 443 """) 444 445 File.touch!("lib/sub/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}}) 446 Mix.Tasks.Format.run([]) 447 448 assert File.read!("lib/sub/a.ex") == """ 449 my_fun(:foo, :bar) 450 other_fun(:baz) 451 """ 452 453 # Add a new entry to "lib" and it also gets picked. 454 File.mkdir_p!("lib/extra") 455 456 File.write!("lib/extra/.formatter.exs", """ 457 [inputs: "a.ex", locals_without_parens: [other_fun: 1]] 458 """) 459 460 File.write!("lib/extra/a.ex", """ 461 my_fun :foo, :bar 462 other_fun :baz 463 """) 464 465 File.touch!("lib/extra/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}}) 466 Mix.Tasks.Format.run([]) 467 468 {_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("lib/extra/a.ex") 469 assert [other_fun: 1] = Keyword.get(formatter_opts, :locals_without_parens) 470 471 assert File.read!("lib/extra/a.ex") == """ 472 my_fun(:foo, :bar) 473 other_fun :baz 474 """ 475 end) 476 end 477 478 test "validates subdirectories in :subdirectories", context do 479 in_tmp(context.test, fn -> 480 File.write!(".formatter.exs", """ 481 [subdirectories: "oops"] 482 """) 483 484 message = "Expected :subdirectories to return a list of directories, got: \"oops\"" 485 assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end 486 487 File.write!(".formatter.exs", """ 488 [subdirectories: ["lib"]] 489 """) 490 491 File.mkdir_p!("lib") 492 493 File.write!("lib/.formatter.exs", """ 494 [] 495 """) 496 497 message = "Expected :inputs or :subdirectories key in lib/.formatter.exs" 498 assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end 499 end) 500 end 501 502 test "validates dependencies in :import_deps", context do 503 in_tmp(context.test, fn -> 504 Mix.Project.push(__MODULE__.FormatWithDepsApp) 505 506 File.write!(".formatter.exs", """ 507 [import_deps: [:my_dep]] 508 """) 509 510 message = 511 "Unavailable dependency :my_dep given to :import_deps in the formatter configuration. " <> 512 "The dependency cannot be found in the file system, please run \"mix deps.get\" and try again" 513 514 assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end 515 516 File.write!(".formatter.exs", """ 517 [import_deps: [:nonexistent_dep]] 518 """) 519 520 message = 521 "Unknown dependency :nonexistent_dep given to :import_deps in the formatter configuration. " <> 522 "The dependency is not listed in your mix.exs for environment :dev" 523 524 assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end 525 end) 526 end 527 528 test "prints an error on conflicting .formatter.exs files", context do 529 in_tmp(context.test, fn -> 530 File.write!(".formatter.exs", """ 531 [inputs: "lib/**/*.{ex,exs}", subdirectories: ["lib", "foo"]] 532 """) 533 534 File.mkdir_p!("lib") 535 536 File.write!("lib/.formatter.exs", """ 537 [inputs: "a.ex", locals_without_parens: [my_fun: 2]] 538 """) 539 540 File.mkdir_p!("foo") 541 542 File.write!("foo/.formatter.exs", """ 543 [inputs: "../lib/a.ex", locals_without_parens: [my_fun: 2]] 544 """) 545 546 File.write!("lib/a.ex", """ 547 my_fun :foo, :bar 548 other_fun :baz 549 """) 550 551 Mix.Tasks.Format.run([]) 552 553 message1 = 554 "Both .formatter.exs and lib/.formatter.exs specify the file lib/a.ex in their " <> 555 ":inputs option. To resolve the conflict, the configuration in .formatter.exs " <> 556 "will be ignored. Please change the list of :inputs in one of the formatter files " <> 557 "so only one of them matches lib/a.ex" 558 559 message2 = 560 "Both lib/.formatter.exs and foo/.formatter.exs specify the file lib/a.ex in their " <> 561 ":inputs option. To resolve the conflict, the configuration in lib/.formatter.exs " <> 562 "will be ignored. Please change the list of :inputs in one of the formatter files " <> 563 "so only one of them matches lib/a.ex" 564 565 assert_received {:mix_shell, :error, [^message1]} 566 assert_received {:mix_shell, :error, [^message2]} 567 end) 568 end 569 570 test "raises on invalid arguments", context do 571 in_tmp(context.test, fn -> 572 assert_raise Mix.Error, ~r"Expected one or more files\/patterns to be given", fn -> 573 Mix.Tasks.Format.run([]) 574 end 575 576 assert_raise Mix.Error, ~r"Could not find a file to format", fn -> 577 Mix.Tasks.Format.run(["unknown.whatever"]) 578 end 579 end) 580 end 581 582 test "raises SyntaxError when parsing invalid source file", context do 583 in_tmp(context.test, fn -> 584 File.write!("a.ex", """ 585 defmodule <%= module %>.Bar do end 586 """) 587 588 assert_raise SyntaxError, ~r"a.ex:1:13: syntax error before: '='", fn -> 589 Mix.Tasks.Format.run(["a.ex"]) 590 end 591 592 assert_received {:mix_shell, :error, ["mix format failed for file: a.ex"]} 593 end) 594 end 595 596 test "raises SyntaxError when parsing invalid stdin", context do 597 in_tmp(context.test, fn -> 598 assert_raise SyntaxError, ~r"stdin:1:13: syntax error before: '='", fn -> 599 capture_io("defmodule <%= module %>.Bar do end", fn -> 600 Mix.Tasks.Format.run(["-"]) 601 end) 602 end 603 604 assert_received {:mix_shell, :error, ["mix format failed for stdin"]} 605 end) 606 end 607end 608