1Code.require_file("../../test_helper.exs", __DIR__) 2 3defmodule Mix.Tasks.XrefTest do 4 use MixTest.Case 5 6 import ExUnit.CaptureIO 7 8 setup_all do 9 previous = Application.get_env(:elixir, :ansi_enabled, false) 10 Application.put_env(:elixir, :ansi_enabled, false) 11 on_exit(fn -> Application.put_env(:elixir, :ansi_enabled, previous) end) 12 end 13 14 setup do 15 Mix.Project.push(MixTest.Case.Sample) 16 :ok 17 end 18 19 describe "calls/1" do 20 test "returns all function calls" do 21 files = %{ 22 "lib/a.ex" => """ 23 defmodule A do 24 def a, do: A.a() 25 def a(arg), do: A.a(arg) 26 def c, do: B.a() 27 end 28 """, 29 "lib/b.ex" => """ 30 defmodule B do 31 def a, do: nil 32 end 33 """ 34 } 35 36 output = [ 37 %{callee: {A, :a, 0}, caller_module: A, file: "lib/a.ex", line: 2}, 38 %{callee: {A, :a, 1}, caller_module: A, file: "lib/a.ex", line: 3}, 39 %{callee: {B, :a, 0}, caller_module: A, file: "lib/a.ex", line: 4} 40 ] 41 42 assert_all_calls(files, output) 43 end 44 45 test "returns function call inside expanded macro" do 46 files = %{ 47 "lib/a.ex" => """ 48 defmodule A do 49 defmacro a_macro(x) do 50 quote do 51 A.b(unquote(x)) 52 end 53 end 54 def b(x), do: x 55 end 56 """, 57 "lib/b.ex" => """ 58 defmodule B do 59 require A 60 def a, do: A.a_macro(1) 61 end 62 """ 63 } 64 65 output = [ 66 %{callee: {A, :b, 1}, caller_module: B, file: "lib/b.ex", line: 3} 67 ] 68 69 assert_all_calls(files, output) 70 end 71 72 test "returns empty on cover compiled modules" do 73 files = %{ 74 "lib/a.ex" => """ 75 defmodule A do 76 def a, do: A.a() 77 end 78 """ 79 } 80 81 assert_all_calls(files, [], fn -> 82 :cover.start() 83 :cover.compile_beam_directory(to_charlist(Mix.Project.compile_path())) 84 end) 85 after 86 :cover.stop() 87 end 88 89 defp assert_all_calls(files, expected, after_compile \\ fn -> :ok end) do 90 in_fixture("no_mixfile", fn -> 91 generate_files(files) 92 93 Mix.Task.run("compile") 94 after_compile.() 95 assert Enum.sort(Mix.Tasks.Xref.calls()) == Enum.sort(expected) 96 end) 97 end 98 end 99 100 describe "mix xref callers MODULE" do 101 @callers_files %{ 102 "lib/a.ex" => """ 103 defmodule A do 104 def a, do: :ok 105 end 106 """, 107 "lib/b.ex" => """ 108 defmodule B do 109 def b, do: A.a() 110 end 111 """ 112 } 113 114 @callers_output """ 115 Compiling 2 files (.ex) 116 Generated sample app 117 lib/b.ex (runtime) 118 """ 119 120 test "prints callers of specified Module" do 121 assert_callers("A", @callers_files, @callers_output) 122 end 123 124 test "filter by compile-connected label with fail-above" do 125 message = "Too many references (found: 1, permitted: 0)" 126 127 assert_raise Mix.Error, message, fn -> 128 assert_callers(~w[--fail-above 0], "A", @callers_files, @callers_output) 129 end 130 end 131 132 test "handles aliases" do 133 files = %{ 134 "lib/a.ex" => """ 135 defmodule A do 136 alias Enum, as: E 137 138 def a(a, b), do: E.map(a, b) 139 140 @file "lib/external_source.ex" 141 def b() do 142 alias Enum, as: EE 143 EE.map([], &EE.flatten/1) 144 end 145 end 146 """ 147 } 148 149 output = """ 150 Compiling 2 files (.ex) 151 Generated sample app 152 lib/a.ex (runtime) 153 """ 154 155 assert_callers("Enum", files, output) 156 end 157 158 test "handles imports" do 159 files = %{ 160 "lib/a.ex" => ~S""" 161 defmodule A do 162 import Integer 163 &is_even/1 164 end 165 """, 166 "lib/b.ex" => ~S""" 167 defmodule B do 168 import Integer 169 parse("1") 170 end 171 """ 172 } 173 174 output = """ 175 Compiling 2 files (.ex) 176 Generated sample app 177 lib/a.ex (compile) 178 lib/b.ex (compile) 179 """ 180 181 assert_callers("Integer", files, output) 182 end 183 184 test "no argument gives error" do 185 in_fixture("no_mixfile", fn -> 186 message = "xref doesn't support this command. For more information run \"mix help xref\"" 187 188 assert_raise Mix.Error, message, fn -> 189 assert Mix.Task.run("xref", ["callers"]) == :error 190 end 191 end) 192 end 193 194 test "callers: gives nice error for quotable but invalid callers spec" do 195 in_fixture("no_mixfile", fn -> 196 message = "xref callers MODULE expects a MODULE, got: Module.func(arg)" 197 198 assert_raise Mix.Error, message, fn -> 199 Mix.Task.run("xref", ["callers", "Module.func(arg)"]) 200 end 201 end) 202 end 203 204 test "gives nice error for unquotable callers spec" do 205 in_fixture("no_mixfile", fn -> 206 message = "xref callers MODULE expects a MODULE, got: %" 207 208 assert_raise Mix.Error, message, fn -> 209 Mix.Task.run("xref", ["callers", "%"]) 210 end 211 end) 212 end 213 214 defp assert_callers(opts \\ [], module, files, expected) do 215 in_fixture("no_mixfile", fn -> 216 for {file, contents} <- files do 217 File.write!(file, contents) 218 end 219 220 capture_io(:stderr, fn -> 221 assert Mix.Task.run("xref", opts ++ ["callers", module]) == :ok 222 end) 223 224 assert ^expected = receive_until_no_messages([]) 225 end) 226 end 227 end 228 229 describe "mix xref trace FILE" do 230 test "shows labelled traces" do 231 files = %{ 232 "lib/a.ex" => ~S""" 233 defmodule A do 234 defstruct [:foo, :bar] 235 defmacro macro, do: :ok 236 def fun, do: :ok 237 end 238 """, 239 "lib/b.ex" => ~S""" 240 defmodule B do 241 import A 242 A.macro() 243 macro() 244 A.fun() 245 fun() 246 def calls_macro, do: A.macro() 247 def calls_fun, do: A.fun() 248 def calls_struct, do: %A{} 249 end 250 """ 251 } 252 253 output = """ 254 Compiling 2 files (.ex) 255 Generated sample app 256 lib/b.ex:2: require A (export) 257 lib/b.ex:3: call A.macro/0 (compile) 258 lib/b.ex:4: import A.macro/0 (compile) 259 lib/b.ex:5: call A.fun/0 (compile) 260 lib/b.ex:6: call A.fun/0 (compile) 261 lib/b.ex:6: import A.fun/0 (compile) 262 lib/b.ex:7: call A.macro/0 (compile) 263 lib/b.ex:8: call A.fun/0 (runtime) 264 lib/b.ex:9: struct A (export) 265 """ 266 267 assert_trace("lib/b.ex", files, output) 268 end 269 270 test "filters per label" do 271 files = %{ 272 "lib/a.ex" => ~S""" 273 defmodule A do 274 defmacro macro, do: :ok 275 def fun, do: :ok 276 end 277 """, 278 "lib/b.ex" => ~S""" 279 defmodule B do 280 require A 281 def calls_macro, do: A.macro() 282 def calls_fun, do: A.fun() 283 end 284 """ 285 } 286 287 output = """ 288 Compiling 2 files (.ex) 289 Generated sample app 290 lib/b.ex:3: call A.macro/0 (compile) 291 """ 292 293 assert_trace(~w[--label compile], "lib/b.ex", files, output) 294 end 295 296 test "fails if above limit per label" do 297 files = %{ 298 "lib/a.ex" => ~S""" 299 defmodule A do 300 defmacro macro, do: :ok 301 def fun, do: :ok 302 end 303 """, 304 "lib/b.ex" => ~S""" 305 defmodule B do 306 require A 307 def calls_macro, do: A.macro() 308 def calls_fun, do: A.fun() 309 end 310 """ 311 } 312 313 output = """ 314 Compiling 2 files (.ex) 315 Generated sample app 316 lib/b.ex:3: call A.macro/0 (compile) 317 """ 318 319 message = "Too many traces (found: 1, permitted: 0)" 320 321 assert_raise Mix.Error, message, fn -> 322 assert_trace(~w[--label compile --fail-above 0], "lib/b.ex", files, output) 323 end 324 end 325 326 defp assert_trace(opts \\ [], file, files, expected) do 327 in_fixture("no_mixfile", fn -> 328 for {file, contents} <- files do 329 File.write!(file, contents) 330 end 331 332 capture_io(:stderr, fn -> 333 assert Mix.Task.run("xref", opts ++ ["trace", file]) == :ok 334 end) 335 336 assert ^expected = receive_until_no_messages([]) 337 end) 338 end 339 end 340 341 describe "mix xref graph" do 342 test "basic usage" do 343 assert_graph(""" 344 lib/a.ex 345 `-- lib/b.ex (compile) 346 lib/b.ex 347 |-- lib/a.ex 348 |-- lib/c.ex 349 `-- lib/e.ex (compile) 350 lib/c.ex 351 `-- lib/d.ex (compile) 352 lib/d.ex 353 `-- lib/e.ex 354 lib/e.ex 355 """) 356 end 357 358 test "stats" do 359 assert_graph(["--format", "stats"], """ 360 Tracked files: 5 (nodes) 361 Compile dependencies: 3 (edges) 362 Exports dependencies: 0 (edges) 363 Runtime dependencies: 3 (edges) 364 Cycles: 1 365 366 Top 5 files with most outgoing dependencies: 367 * lib/b.ex (3) 368 * lib/d.ex (1) 369 * lib/c.ex (1) 370 * lib/a.ex (1) 371 * lib/e.ex (0) 372 373 Top 5 files with most incoming dependencies: 374 * lib/e.ex (2) 375 * lib/d.ex (1) 376 * lib/c.ex (1) 377 * lib/b.ex (1) 378 * lib/a.ex (1) 379 """) 380 end 381 382 test "cycles" do 383 assert_graph(["--format", "cycles"], """ 384 1 cycles found. Showing them in decreasing size: 385 386 Cycle of length 3: 387 388 lib/b.ex 389 lib/a.ex 390 lib/b.ex 391 392 """) 393 end 394 395 test "cycles with `--fail-above`" do 396 message = "Too many cycles (found: 1, permitted: 0)" 397 398 assert_raise Mix.Error, message, fn -> 399 assert_graph(["--format", "cycles", "--fail-above", "0"], """ 400 1 cycles found. Showing them in decreasing size: 401 402 Cycle of length 3: 403 404 lib/b.ex 405 lib/a.ex 406 lib/b.ex 407 408 """) 409 end 410 end 411 412 test "cycles with min cycle size" do 413 assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """ 414 No cycles found 415 """) 416 end 417 418 test "unknown label" do 419 assert_raise Mix.Error, "Unknown --label bad in mix xref graph", fn -> 420 assert_graph(["--label", "bad"], "") 421 end 422 end 423 424 test "unknown format" do 425 assert_raise Mix.Error, "Unknown --format bad in mix xref graph", fn -> 426 assert_graph(["--format", "bad"], "") 427 end 428 end 429 430 test "exclude many" do 431 assert_graph(~w[--exclude lib/c.ex --exclude lib/b.ex], """ 432 lib/a.ex 433 lib/d.ex 434 `-- lib/e.ex 435 lib/e.ex 436 """) 437 end 438 439 test "exclude one" do 440 assert_graph(~w[--exclude lib/d.ex], """ 441 lib/a.ex 442 `-- lib/b.ex (compile) 443 lib/b.ex 444 |-- lib/a.ex 445 |-- lib/c.ex 446 `-- lib/e.ex (compile) 447 lib/c.ex 448 lib/e.ex 449 """) 450 end 451 452 @abc_linear_files %{ 453 "lib/a.ex" => "defmodule A, do: def a, do: B.b()", 454 "lib/b.ex" => "defmodule B, do: def b, do: C.c()", 455 "lib/c.ex" => "defmodule C, do: def c, do: true" 456 } 457 458 test "exclude one from linear case" do 459 assert_graph( 460 ~w[--exclude lib/b.ex], 461 """ 462 lib/a.ex 463 lib/c.ex 464 """, 465 files: @abc_linear_files 466 ) 467 end 468 469 test "exclude one with source from linear case" do 470 assert_graph( 471 ~w[--exclude lib/b.ex --source lib/a.ex], 472 """ 473 lib/a.ex 474 """, 475 files: @abc_linear_files 476 ) 477 end 478 479 test "only nodes" do 480 assert_graph(~w[--only-nodes], """ 481 lib/a.ex 482 lib/b.ex 483 lib/c.ex 484 lib/d.ex 485 lib/e.ex 486 """) 487 end 488 489 test "only nodes with compile direct label" do 490 assert_graph(~w[--label compile --only-direct --only-nodes], """ 491 lib/a.ex 492 lib/b.ex 493 lib/c.ex 494 """) 495 end 496 497 test "filter by compile label" do 498 assert_graph(~w[--label compile], """ 499 lib/a.ex 500 `-- lib/b.ex (compile) 501 lib/b.ex 502 `-- lib/e.ex (compile) 503 lib/c.ex 504 `-- lib/d.ex (compile) 505 """) 506 end 507 508 test "filter by compile-connected label" do 509 assert_graph(~w[--label compile-connected], """ 510 lib/a.ex 511 `-- lib/b.ex (compile) 512 lib/c.ex 513 `-- lib/d.ex (compile) 514 """) 515 end 516 517 test "filter by compile-connected label with exclusions" do 518 assert_graph(~w[--label compile-connected --exclude lib/e.ex], """ 519 lib/a.ex 520 `-- lib/b.ex (compile) 521 """) 522 end 523 524 test "filter by compile-connected label with fail-above" do 525 message = "Too many references (found: 2, permitted: 1)" 526 527 assert_raise Mix.Error, message, fn -> 528 assert_graph(~w[--label compile-connected --fail-above 1], """ 529 lib/a.ex 530 `-- lib/b.ex (compile) 531 lib/c.ex 532 `-- lib/d.ex (compile) 533 """) 534 end 535 end 536 537 test "exclude many with fail-above" do 538 message = "Too many references (found: 1, permitted: 0)" 539 540 assert_raise Mix.Error, message, fn -> 541 assert_graph(~w[--exclude lib/c.ex --exclude lib/b.ex --fail-above 0], """ 542 lib/a.ex 543 lib/d.ex 544 `-- lib/e.ex 545 lib/e.ex 546 """) 547 end 548 end 549 550 test "filter by compile direct label" do 551 assert_graph(~w[--label compile --only-direct], """ 552 lib/a.ex 553 `-- lib/b.ex (compile) 554 lib/b.ex 555 `-- lib/e.ex (compile) 556 lib/c.ex 557 `-- lib/d.ex (compile) 558 """) 559 end 560 561 test "filter by runtime label" do 562 assert_graph(~w[--label runtime], """ 563 lib/b.ex 564 |-- lib/a.ex 565 `-- lib/c.ex 566 lib/d.ex 567 `-- lib/e.ex 568 """) 569 end 570 571 test "sources" do 572 assert_graph(~w[--source lib/a.ex --source lib/c.ex], """ 573 lib/a.ex 574 `-- lib/b.ex (compile) 575 |-- lib/a.ex 576 |-- lib/c.ex 577 `-- lib/e.ex (compile) 578 lib/c.ex 579 `-- lib/d.ex (compile) 580 `-- lib/e.ex 581 """) 582 end 583 584 test "source with compile label" do 585 assert_graph(~w[--source lib/a.ex --label compile], """ 586 lib/a.ex 587 `-- lib/b.ex (compile) 588 `-- lib/e.ex (compile) 589 """) 590 end 591 592 test "source with compile-connected label" do 593 assert_graph(~w[--source lib/a.ex --label compile-connected], """ 594 lib/a.ex 595 `-- lib/b.ex (compile) 596 """) 597 end 598 599 test "source with compile direct label" do 600 assert_graph(~w[--source lib/a.ex --label compile --only-direct], """ 601 lib/a.ex 602 `-- lib/b.ex (compile) 603 `-- lib/e.ex (compile) 604 """) 605 end 606 607 test "invalid sources" do 608 assert_raise Mix.Error, "Sources could not be found: lib/a2.ex, lib/a3.ex", fn -> 609 assert_graph(~w[--source lib/a2.ex --source lib/a.ex --source lib/a3.ex], "") 610 end 611 end 612 613 test "sink" do 614 assert_graph(~w[--sink lib/e.ex], """ 615 lib/a.ex 616 `-- lib/b.ex (compile) 617 lib/b.ex 618 |-- lib/a.ex 619 |-- lib/c.ex 620 `-- lib/e.ex (compile) 621 lib/c.ex 622 `-- lib/d.ex (compile) 623 lib/d.ex 624 `-- lib/e.ex 625 """) 626 end 627 628 test "sink with compile label" do 629 assert_graph(~w[--sink lib/e.ex --label compile], """ 630 lib/a.ex 631 `-- lib/b.ex (compile) 632 lib/b.ex 633 `-- lib/e.ex (compile) 634 lib/c.ex 635 `-- lib/d.ex (compile) 636 """) 637 end 638 639 test "sink with compile-connected label" do 640 assert_graph(~w[--sink lib/e.ex --label compile-connected], """ 641 lib/a.ex 642 `-- lib/b.ex (compile) 643 lib/c.ex 644 `-- lib/d.ex (compile) 645 """) 646 end 647 648 test "sink with compile direct label" do 649 assert_graph(~w[--sink lib/e.ex --label compile --only-direct], """ 650 lib/a.ex 651 `-- lib/b.ex (compile) 652 lib/b.ex 653 `-- lib/e.ex (compile) 654 """) 655 end 656 657 test "multiple sinks" do 658 assert_graph(~w[--sink lib/a.ex --sink lib/c.ex], """ 659 lib/b.ex 660 |-- lib/a.ex 661 | `-- lib/b.ex (compile) 662 `-- lib/c.ex 663 """) 664 end 665 666 test "multiple sinks with only nodes" do 667 assert_graph(~w[--sink lib/a.ex --sink lib/c.ex --sink lib/e.ex --only-nodes], """ 668 lib/b.ex 669 lib/d.ex 670 """) 671 end 672 673 test "invalid sink" do 674 assert_raise Mix.Error, "Sinks could not be found: lib/b2.ex, lib/b3.ex", fn -> 675 assert_graph(~w[--sink lib/b2.ex --sink lib/b.ex --sink lib/b3.ex], "") 676 end 677 end 678 679 test "sink and source" do 680 assert_graph(~w[--source lib/a.ex --sink lib/b.ex], """ 681 lib/a.ex 682 `-- lib/b.ex (compile) 683 `-- lib/a.ex 684 """) 685 end 686 687 test "with dynamic module" do 688 in_fixture("no_mixfile", fn -> 689 File.write!("lib/a.ex", """ 690 B.define() 691 """) 692 693 File.write!("lib/b.ex", """ 694 defmodule B do 695 def define do 696 defmodule A do 697 end 698 end 699 end 700 """) 701 702 assert Mix.Task.run("xref", ["graph", "--format", "dot"]) == :ok 703 704 assert File.read!("xref_graph.dot") === """ 705 digraph "xref graph" { 706 "lib/a.ex" 707 "lib/a.ex" -> "lib/b.ex" [label="(compile)"] 708 "lib/b.ex" 709 } 710 """ 711 end) 712 end 713 714 test "with export" do 715 in_fixture("no_mixfile", fn -> 716 File.write!("lib/a.ex", """ 717 defmodule A do 718 def fun do 719 %B{} 720 end 721 end 722 """) 723 724 File.write!("lib/b.ex", """ 725 defmodule B do 726 defstruct [] 727 end 728 """) 729 730 assert Mix.Task.run("xref", ["graph", "--format", "dot"]) == :ok 731 732 assert File.read!("xref_graph.dot") === """ 733 digraph "xref graph" { 734 "lib/a.ex" 735 "lib/a.ex" -> "lib/b.ex" [label="(export)"] 736 "lib/b.ex" 737 } 738 """ 739 end) 740 end 741 742 test "with mixed cyclic dependencies" do 743 in_fixture("no_mixfile", fn -> 744 File.write!("lib/a.ex", """ 745 defmodule A.Behaviour do 746 @callback foo :: :foo 747 end 748 749 defmodule A do 750 B 751 752 def foo do 753 :foo 754 end 755 end 756 """) 757 758 File.write!("lib/b.ex", """ 759 defmodule B do 760 # Let's also test that we track literal atom behaviours 761 @behaviour :"Elixir.A.Behaviour" 762 763 def foo do 764 A.foo() 765 end 766 end 767 """) 768 769 assert Mix.Task.run("xref", ["graph", "--format", "dot"]) == :ok 770 771 assert File.read!("xref_graph.dot") === """ 772 digraph "xref graph" { 773 "lib/a.ex" 774 "lib/a.ex" -> "lib/b.ex" [label="(compile)"] 775 "lib/b.ex" -> "lib/a.ex" [label="(export)"] 776 "lib/b.ex" 777 } 778 """ 779 end) 780 end 781 782 test "generates reports considering siblings inside umbrellas" do 783 Mix.Project.pop() 784 785 in_fixture("umbrella_dep/deps/umbrella", fn -> 786 Mix.Project.in_project(:bar, "apps/bar", fn _ -> 787 File.write!("lib/bar.ex", """ 788 defmodule Bar do 789 def bar do 790 Foo.foo() 791 end 792 end 793 """) 794 795 Mix.Task.run("compile") 796 Mix.shell().flush() 797 798 Mix.Tasks.Xref.run(["graph", "--format", "stats", "--include-siblings"]) 799 800 assert receive_until_no_messages([]) == """ 801 Tracked files: 2 (nodes) 802 Compile dependencies: 0 (edges) 803 Exports dependencies: 0 (edges) 804 Runtime dependencies: 1 (edges) 805 Cycles: 0 806 807 Top 2 files with most outgoing dependencies: 808 * lib/bar.ex (1) 809 * lib/foo.ex (0) 810 811 Top 2 files with most incoming dependencies: 812 * lib/foo.ex (1) 813 * lib/bar.ex (0) 814 """ 815 816 Mix.Tasks.Xref.run(["callers", "Foo"]) 817 818 assert receive_until_no_messages([]) == """ 819 lib/bar.ex (runtime) 820 """ 821 end) 822 end) 823 end 824 825 test "skip project compilation with --no-compile" do 826 in_fixture("no_mixfile", fn -> 827 File.write!("lib/a.ex", """ 828 defmodule A do 829 def a, do: :ok 830 end 831 """) 832 833 Mix.Tasks.Xref.run(["graph", "--no-compile"]) 834 refute receive_until_no_messages([]) =~ "lib/a.ex" 835 end) 836 end 837 838 @default_files %{ 839 "lib/a.ex" => """ 840 defmodule A do 841 def a, do: :ok 842 B.b2() 843 end 844 """, 845 "lib/b.ex" => """ 846 defmodule B do 847 def b1, do: A.a() == C.c() 848 def b2, do: :ok 849 :e.e() 850 end 851 """, 852 "lib/c.ex" => """ 853 defmodule C do 854 def c, do: :ok 855 :d.d() 856 end 857 """, 858 "lib/d.ex" => """ 859 defmodule :d do 860 def d, do: :ok 861 def e, do: :e.e() 862 end 863 """, 864 "lib/e.ex" => """ 865 defmodule :e do 866 def e, do: :ok 867 end 868 """ 869 } 870 871 defp assert_graph(opts \\ [], expected, params \\ []) do 872 in_fixture("no_mixfile", fn -> 873 nb_files = 874 Enum.count(params[:files] || @default_files, fn {path, content} -> 875 File.write!(path, content) 876 end) 877 878 assert Mix.Task.run("xref", opts ++ ["graph"]) == :ok 879 first_line = "Compiling #{nb_files} files (.ex)" 880 881 assert [ 882 ^first_line | ["Generated sample app" | result] 883 ] = receive_until_no_messages([]) |> String.split("\n") 884 885 assert normalize_graph_output(result |> Enum.join("\n")) == expected 886 end) 887 end 888 889 defp normalize_graph_output(graph) do 890 graph 891 |> String.replace("├──", "|--") 892 |> String.replace("└──", "`--") 893 |> String.replace("│", "|") 894 end 895 end 896 897 ## Helpers 898 899 defp receive_until_no_messages(acc) do 900 receive do 901 {:mix_shell, :info, [line]} -> receive_until_no_messages([acc, line | "\n"]) 902 after 903 0 -> IO.iodata_to_binary(acc) 904 end 905 end 906 907 defp generate_files(files) do 908 for {file, contents} <- files do 909 File.write!(file, contents) 910 end 911 end 912end 913