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