1import os
2import subprocess
3import sys
4from textwrap import dedent
5
6import mock
7import pytest
8from pytest import mark
9
10from .constants import MINIMAL_WHEELS_PATH, PACKAGES_PATH
11from .utils import invoke
12
13from piptools._compat.pip_compat import PIP_VERSION, path_to_url
14from piptools.scripts.compile import cli
15
16
17@pytest.fixture(autouse=True)
18def temp_dep_cache(tmpdir, monkeypatch):
19    monkeypatch.setenv("PIP_TOOLS_CACHE_DIR", str(tmpdir / "cache"))
20
21
22def test_default_pip_conf_read(pip_with_index_conf, runner):
23    # preconditions
24    with open("requirements.in", "w"):
25        pass
26    out = runner.invoke(cli, ["-v"])
27
28    # check that we have our index-url as specified in pip.conf
29    assert "Using indexes:\n  http://example.com" in out.stderr
30    assert "--index-url http://example.com" in out.stderr
31
32
33def test_command_line_overrides_pip_conf(pip_with_index_conf, runner):
34    # preconditions
35    with open("requirements.in", "w"):
36        pass
37    out = runner.invoke(cli, ["-v", "-i", "http://override.com"])
38
39    # check that we have our index-url as specified in pip.conf
40    assert "Using indexes:\n  http://override.com" in out.stderr
41
42
43def test_command_line_setuptools_read(pip_conf, runner):
44    with open("setup.py", "w") as package:
45        package.write(
46            dedent(
47                """\
48                from setuptools import setup
49                setup(
50                    name="fake-setuptools-a",
51                    install_requires=["small-fake-a==0.1"]
52                )
53                """
54            )
55        )
56    out = runner.invoke(cli)
57
58    assert "This file is autogenerated by pip-compile" in out.stderr
59    assert (
60        "small-fake-a==0.1         # via fake-setuptools-a (setup.py)"
61        in out.stderr.splitlines()
62    )
63
64    # check that pip-compile generated a configuration file
65    assert os.path.exists("requirements.txt")
66
67
68@pytest.mark.parametrize(
69    "options, expected_output_file",
70    [
71        # For the `pip-compile` output file should be "requirements.txt"
72        ([], "requirements.txt"),
73        # For the `pip-compile --output-file=output.txt`
74        # output file should be "output.txt"
75        (["--output-file", "output.txt"], "output.txt"),
76        # For the `pip-compile setup.py` output file should be "requirements.txt"
77        (["setup.py"], "requirements.txt"),
78        # For the `pip-compile setup.py --output-file=output.txt`
79        # output file should be "output.txt"
80        (["setup.py", "--output-file", "output.txt"], "output.txt"),
81    ],
82)
83def test_command_line_setuptools_output_file(
84    pip_conf, runner, options, expected_output_file
85):
86    """
87    Test the output files for setup.py as a requirement file.
88    """
89    with open("setup.py", "w") as package:
90        package.write(
91            dedent(
92                """\
93                from setuptools import setup
94                setup(install_requires=[])
95                """
96            )
97        )
98
99    out = runner.invoke(cli, options)
100    assert out.exit_code == 0
101    assert os.path.exists(expected_output_file)
102
103
104def test_find_links_option(runner):
105    with open("requirements.in", "w") as req_in:
106        req_in.write("-f ./libs3")
107
108    out = runner.invoke(cli, ["-v", "-f", "./libs1", "-f", "./libs2"])
109
110    # Check that find-links has been passed to pip
111    assert "Configuration:\n  -f ./libs1\n  -f ./libs2\n  -f ./libs3\n" in out.stderr
112
113    # Check that find-links has been written to a requirements.txt
114    with open("requirements.txt", "r") as req_txt:
115        assert (
116            "--find-links ./libs1\n--find-links ./libs2\n--find-links ./libs3\n"
117            in req_txt.read()
118        )
119
120
121def test_extra_index_option(pip_with_index_conf, runner):
122    with open("requirements.in", "w"):
123        pass
124    out = runner.invoke(
125        cli,
126        [
127            "-v",
128            "--extra-index-url",
129            "http://extraindex1.com",
130            "--extra-index-url",
131            "http://extraindex2.com",
132        ],
133    )
134    assert (
135        "Using indexes:\n"
136        "  http://example.com\n"
137        "  http://extraindex1.com\n"
138        "  http://extraindex2.com" in out.stderr
139    )
140    assert (
141        "--index-url http://example.com\n"
142        "--extra-index-url http://extraindex1.com\n"
143        "--extra-index-url http://extraindex2.com" in out.stderr
144    )
145
146
147def test_trusted_host(pip_conf, runner):
148    with open("requirements.in", "w"):
149        pass
150    out = runner.invoke(
151        cli, ["-v", "--trusted-host", "example.com", "--trusted-host", "example2.com"]
152    )
153    assert "--trusted-host example.com\n" "--trusted-host example2.com\n" in out.stderr
154
155
156def test_trusted_host_no_emit(pip_conf, runner):
157    with open("requirements.in", "w"):
158        pass
159    out = runner.invoke(
160        cli, ["-v", "--trusted-host", "example.com", "--no-emit-trusted-host"]
161    )
162    assert "--trusted-host example.com" not in out.stderr
163
164
165def test_find_links_no_emit(pip_conf, runner):
166    with open("requirements.in", "w"):
167        pass
168    out = runner.invoke(cli, ["-v", "--no-emit-find-links"])
169    assert "--find-links" not in out.stderr
170
171
172@pytest.mark.network
173def test_realistic_complex_sub_dependencies(runner):
174    wheels_dir = "wheels"
175
176    # make a temporary wheel of a fake package
177    subprocess.check_output(
178        [
179            "pip",
180            "wheel",
181            "--no-deps",
182            "-w",
183            wheels_dir,
184            os.path.join(PACKAGES_PATH, "fake_with_deps", "."),
185        ]
186    )
187
188    with open("requirements.in", "w") as req_in:
189        req_in.write("fake_with_deps")  # require fake package
190
191    out = runner.invoke(cli, ["-v", "-n", "--rebuild", "-f", wheels_dir])
192
193    assert out.exit_code == 0
194
195
196def test_run_as_module_compile():
197    """piptools can be run as ``python -m piptools ...``."""
198
199    status, output = invoke([sys.executable, "-m", "piptools", "compile", "--help"])
200
201    # Should have run pip-compile successfully.
202    output = output.decode("utf-8")
203    assert output.startswith("Usage:")
204    assert "Compiles requirements.txt from requirements.in" in output
205    assert status == 0
206
207
208def test_editable_package(pip_conf, runner):
209    """ piptools can compile an editable """
210    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
211    fake_package_dir = path_to_url(fake_package_dir)
212    with open("requirements.in", "w") as req_in:
213        req_in.write("-e " + fake_package_dir)  # require editable fake package
214
215    out = runner.invoke(cli, ["-n"])
216
217    assert out.exit_code == 0
218    assert fake_package_dir in out.stderr
219    assert "small-fake-a==0.1" in out.stderr
220
221
222@pytest.mark.network
223def test_editable_package_vcs(runner):
224    vcs_package = (
225        "git+git://github.com/jazzband/pip-tools@"
226        "f97e62ecb0d9b70965c8eff952c001d8e2722e94"
227        "#egg=pip-tools"
228    )
229    with open("requirements.in", "w") as req_in:
230        req_in.write("-e " + vcs_package)
231    out = runner.invoke(cli, ["-n", "--rebuild"])
232    assert out.exit_code == 0
233    assert vcs_package in out.stderr
234    assert "click" in out.stderr  # dependency of pip-tools
235
236
237def test_locally_available_editable_package_is_not_archived_in_cache_dir(
238    pip_conf, tmpdir, runner
239):
240    """
241    piptools will not create an archive for a locally available editable requirement
242    """
243    cache_dir = tmpdir.mkdir("cache_dir")
244
245    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
246    fake_package_dir = path_to_url(fake_package_dir)
247
248    with open("requirements.in", "w") as req_in:
249        req_in.write("-e " + fake_package_dir)  # require editable fake package
250
251    out = runner.invoke(cli, ["-n", "--rebuild", "--cache-dir", str(cache_dir)])
252
253    assert out.exit_code == 0
254    assert fake_package_dir in out.stderr
255    assert "small-fake-a==0.1" in out.stderr
256
257    # we should not find any archived file in {cache_dir}/pkgs
258    assert not os.listdir(os.path.join(str(cache_dir), "pkgs"))
259
260
261@mark.parametrize(
262    ("line", "dependency"),
263    [
264        # zip URL
265        # use pip-tools version prior to its use of setuptools_scm,
266        # which is incompatible with https: install
267        (
268            "https://github.com/jazzband/pip-tools/archive/"
269            "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip",
270            "\nclick==",
271        ),
272        # scm URL
273        (
274            "git+git://github.com/jazzband/pip-tools@"
275            "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3",
276            "\nclick==",
277        ),
278        # wheel URL
279        (
280            "https://files.pythonhosted.org/packages/06/96/"
281            "89872db07ae70770fba97205b0737c17ef013d0d1c790"
282            "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl",
283            "\nclick==",
284        ),
285    ],
286)
287@mark.parametrize(("generate_hashes",), [(True,), (False,)])
288@pytest.mark.network
289def test_url_package(runner, line, dependency, generate_hashes):
290    with open("requirements.in", "w") as req_in:
291        req_in.write(line)
292    out = runner.invoke(
293        cli, ["-n", "--rebuild"] + (["--generate-hashes"] if generate_hashes else [])
294    )
295    assert out.exit_code == 0
296    assert dependency in out.stderr
297
298
299@mark.parametrize(
300    ("line", "dependency", "rewritten_line"),
301    [
302        # file:// wheel URL
303        (
304            path_to_url(
305                os.path.join(
306                    MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
307                )
308            ),
309            "\nsmall-fake-a==0.1",
310            None,
311        ),
312        # file:// directory
313        (
314            path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_deps")),
315            "\nsmall-fake-a==0.1",
316            None,
317        ),
318        # bare path
319        # will be rewritten to file:// URL
320        (
321            os.path.join(
322                MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
323            ),
324            "\nsmall-fake-a==0.1",
325            path_to_url(
326                os.path.join(
327                    MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
328                )
329            ),
330        ),
331    ],
332)
333@mark.parametrize(("generate_hashes",), [(True,), (False,)])
334def test_local_url_package(
335    pip_conf, runner, line, dependency, rewritten_line, generate_hashes
336):
337    if rewritten_line is None:
338        rewritten_line = line
339    with open("requirements.in", "w") as req_in:
340        req_in.write(line)
341    out = runner.invoke(
342        cli, ["-n", "--rebuild"] + (["--generate-hashes"] if generate_hashes else [])
343    )
344    assert out.exit_code == 0
345    assert rewritten_line in out.stderr
346    assert dependency in out.stderr
347
348
349def test_input_file_without_extension(pip_conf, runner):
350    """
351    piptools can compile a file without an extension,
352    and add .txt as the defaut output file extension.
353    """
354    with open("requirements", "w") as req_in:
355        req_in.write("small-fake-a==0.1")
356
357    out = runner.invoke(cli, ["requirements"])
358
359    assert out.exit_code == 0
360    assert "small-fake-a==0.1" in out.stderr
361    assert os.path.exists("requirements.txt")
362
363
364def test_upgrade_packages_option(pip_conf, runner):
365    """
366    piptools respects --upgrade-package/-P inline list.
367    """
368    with open("requirements.in", "w") as req_in:
369        req_in.write("small-fake-a\nsmall-fake-b")
370    with open("requirements.txt", "w") as req_in:
371        req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1")
372
373    out = runner.invoke(cli, ["--no-annotate", "-P", "small-fake-b"])
374
375    assert out.exit_code == 0
376    assert "small-fake-a==0.1" in out.stderr.splitlines()
377    assert "small-fake-b==0.3" in out.stderr.splitlines()
378
379
380def test_upgrade_packages_option_irrelevant(pip_conf, runner):
381    """
382    piptools ignores --upgrade-package/-P items not already constrained.
383    """
384    with open("requirements.in", "w") as req_in:
385        req_in.write("small-fake-a")
386    with open("requirements.txt", "w") as req_in:
387        req_in.write("small-fake-a==0.1")
388
389    out = runner.invoke(cli, ["--no-annotate", "--upgrade-package", "small-fake-b"])
390
391    assert out.exit_code == 0
392    assert "small-fake-a==0.1" in out.stderr.splitlines()
393    assert "small-fake-b==0.3" not in out.stderr.splitlines()
394
395
396def test_upgrade_packages_option_no_existing_file(pip_conf, runner):
397    """
398    piptools respects --upgrade-package/-P inline list when the output file
399    doesn't exist.
400    """
401    with open("requirements.in", "w") as req_in:
402        req_in.write("small-fake-a\nsmall-fake-b")
403
404    out = runner.invoke(cli, ["--no-annotate", "-P", "small-fake-b"])
405
406    assert out.exit_code == 0
407    assert "small-fake-a==0.2" in out.stderr.splitlines()
408    assert "small-fake-b==0.3" in out.stderr.splitlines()
409
410
411@pytest.mark.parametrize(
412    "current_package, upgraded_package",
413    (
414        pytest.param("small-fake-b==0.1", "small-fake-b==0.3", id="upgrade"),
415        pytest.param("small-fake-b==0.3", "small-fake-b==0.1", id="downgrade"),
416    ),
417)
418def test_upgrade_packages_version_option(
419    pip_conf, runner, current_package, upgraded_package
420):
421    """
422    piptools respects --upgrade-package/-P inline list with specified versions.
423    """
424    with open("requirements.in", "w") as req_in:
425        req_in.write("small-fake-a\nsmall-fake-b")
426    with open("requirements.txt", "w") as req_in:
427        req_in.write("small-fake-a==0.1\n" + current_package)
428
429    out = runner.invoke(cli, ["--no-annotate", "--upgrade-package", upgraded_package])
430
431    assert out.exit_code == 0
432    stderr_lines = out.stderr.splitlines()
433    assert "small-fake-a==0.1" in stderr_lines
434    assert upgraded_package in stderr_lines
435
436
437def test_upgrade_packages_version_option_no_existing_file(pip_conf, runner):
438    """
439    piptools respects --upgrade-package/-P inline list with specified versions.
440    """
441    with open("requirements.in", "w") as req_in:
442        req_in.write("small-fake-a\nsmall-fake-b")
443
444    out = runner.invoke(cli, ["-P", "small-fake-b==0.2"])
445
446    assert out.exit_code == 0
447    assert "small-fake-a==0.2" in out.stderr
448    assert "small-fake-b==0.2" in out.stderr
449
450
451def test_upgrade_packages_version_option_and_upgrade(pip_conf, runner):
452    """
453    piptools respects --upgrade-package/-P inline list with specified versions
454    whilst also doing --upgrade.
455    """
456    with open("requirements.in", "w") as req_in:
457        req_in.write("small-fake-a\nsmall-fake-b")
458    with open("requirements.txt", "w") as req_in:
459        req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1")
460
461    out = runner.invoke(cli, ["--upgrade", "-P", "small-fake-b==0.1"])
462
463    assert out.exit_code == 0
464    assert "small-fake-a==0.2" in out.stderr
465    assert "small-fake-b==0.1" in out.stderr
466
467
468def test_upgrade_packages_version_option_and_upgrade_no_existing_file(pip_conf, runner):
469    """
470    piptools respects --upgrade-package/-P inline list with specified versions
471    whilst also doing --upgrade and the output file doesn't exist.
472    """
473    with open("requirements.in", "w") as req_in:
474        req_in.write("small-fake-a\nsmall-fake-b")
475
476    out = runner.invoke(cli, ["--upgrade", "-P", "small-fake-b==0.1"])
477
478    assert out.exit_code == 0
479    assert "small-fake-a==0.2" in out.stderr
480    assert "small-fake-b==0.1" in out.stderr
481
482
483def test_quiet_option(runner):
484    with open("requirements", "w"):
485        pass
486    out = runner.invoke(cli, ["--quiet", "-n", "requirements"])
487    # Pinned requirements result has not been written to output.
488    assert not out.stderr_bytes
489
490
491def test_dry_run_noisy_option(runner):
492    with open("requirements", "w"):
493        pass
494    out = runner.invoke(cli, ["--dry-run", "requirements"])
495    # Dry-run message has been written to output
496    assert "Dry-run, so nothing updated." in out.stderr.splitlines()
497
498
499def test_dry_run_quiet_option(runner):
500    with open("requirements", "w"):
501        pass
502    out = runner.invoke(cli, ["--dry-run", "--quiet", "requirements"])
503    # Dry-run message has not been written to output.
504    assert not out.stderr_bytes
505
506
507def test_generate_hashes_with_editable(pip_conf, runner):
508    small_fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
509    small_fake_package_url = path_to_url(small_fake_package_dir)
510    with open("requirements.in", "w") as fp:
511        fp.write("-e {}\n".format(small_fake_package_url))
512    out = runner.invoke(cli, ["--no-annotate", "--generate-hashes"])
513    expected = (
514        "-e {}\n"
515        "small-fake-a==0.1 \\\n"
516        "    --hash=sha256:5e6071ee6e4c59e0d0408d366f"
517        "e9b66781d2cf01be9a6e19a2433bb3c5336330\n"
518        "small-fake-b==0.1 \\\n"
519        "    --hash=sha256:acdba8f8b8a816213c30d5310c"
520        "3fe296c0107b16ed452062f7f994a5672e3b3f\n"
521    ).format(small_fake_package_url)
522    assert out.exit_code == 0
523    assert expected in out.stderr
524
525
526@pytest.mark.network
527def test_generate_hashes_with_url(runner):
528    with open("requirements.in", "w") as fp:
529        fp.write(
530            "https://github.com/jazzband/pip-tools/archive/"
531            "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip#egg=pip-tools\n"
532        )
533    out = runner.invoke(cli, ["--no-annotate", "--generate-hashes"])
534    expected = (
535        "https://github.com/jazzband/pip-tools/archive/"
536        "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip#egg=pip-tools \\\n"
537        "    --hash=sha256:d24de92e18ad5bf291f25cfcdcf"
538        "0171be6fa70d01d0bef9eeda356b8549715e7\n"
539    )
540    assert out.exit_code == 0
541    assert expected in out.stderr
542
543
544def test_generate_hashes_verbose(pip_conf, runner):
545    """
546    The hashes generation process should show a progress.
547    """
548    with open("requirements.in", "w") as fp:
549        fp.write("small-fake-a==0.1")
550
551    out = runner.invoke(cli, ["--generate-hashes", "-v"])
552    expected_verbose_text = "Generating hashes:\n  small-fake-a\n"
553    assert expected_verbose_text in out.stderr
554
555
556@pytest.mark.skipif(PIP_VERSION < (9,), reason="needs pip 9 or greater")
557def test_filter_pip_markers(pip_conf, runner):
558    """
559    Check that pip-compile works with pip environment markers (PEP496)
560    """
561    with open("requirements", "w") as req_in:
562        req_in.write(
563            "small-fake-a==0.1\n" "unknown_package==0.1; python_version == '1'"
564        )
565
566    out = runner.invoke(cli, ["-n", "requirements"])
567
568    assert out.exit_code == 0
569    assert "small-fake-a==0.1" in out.stderr
570    assert "unknown_package" not in out.stderr
571
572
573def test_no_candidates(pip_conf, runner):
574    with open("requirements", "w") as req_in:
575        req_in.write("small-fake-a>0.3b1,<0.3b2")
576
577    out = runner.invoke(cli, ["-n", "requirements"])
578
579    assert out.exit_code == 2
580    assert "Skipped pre-versions:" in out.stderr
581
582
583def test_no_candidates_pre(pip_conf, runner):
584    with open("requirements", "w") as req_in:
585        req_in.write("small-fake-a>0.3b1,<0.3b1")
586
587    out = runner.invoke(cli, ["-n", "requirements", "--pre"])
588
589    assert out.exit_code == 2
590    assert "Tried pre-versions:" in out.stderr
591
592
593def test_default_index_url(pip_with_index_conf):
594    status, output = invoke([sys.executable, "-m", "piptools", "compile", "--help"])
595    output = output.decode("utf-8")
596
597    # Click's subprocess output has \r\r\n line endings on win py27. Fix it.
598    output = output.replace("\r\r", "\r")
599
600    assert status == 0
601    expected = (
602        "  -i, --index-url TEXT            Change index URL (defaults to"
603        + os.linesep
604        + "                                  http://example.com)"
605        + os.linesep
606    )
607    assert expected in output
608
609
610def test_stdin_without_output_file(runner):
611    """
612    The --output-file option is required for STDIN.
613    """
614    out = runner.invoke(cli, ["-n", "-"])
615
616    assert out.exit_code == 2
617    assert "--output-file is required if input is from stdin" in out.stderr
618
619
620def test_not_specified_input_file(runner):
621    """
622    It should raise an error if there are no input files or default input files
623    such as "setup.py" or "requirements.in".
624    """
625    out = runner.invoke(cli)
626    assert "If you do not specify an input file" in out.stderr
627    assert out.exit_code == 2
628
629
630def test_stdin(pip_conf, runner):
631    """
632    Test compile requirements from STDIN.
633    """
634    out = runner.invoke(
635        cli, ["-", "--output-file", "requirements.txt", "-n"], input="small-fake-a==0.1"
636    )
637
638    assert "small-fake-a==0.1         # via -r -" in out.stderr.splitlines()
639
640
641def test_multiple_input_files_without_output_file(runner):
642    """
643    The --output-file option is required for multiple requirement input files.
644    """
645    with open("src_file1.in", "w") as req_in:
646        req_in.write("six==1.10.0")
647
648    with open("src_file2.in", "w") as req_in:
649        req_in.write("django==2.1")
650
651    out = runner.invoke(cli, ["src_file1.in", "src_file2.in"])
652
653    assert (
654        "--output-file is required if two or more input files are given" in out.stderr
655    )
656    assert out.exit_code == 2
657
658
659@pytest.mark.parametrize(
660    "option, expected",
661    [
662        (
663            "--annotate",
664            "small-fake-a==0.1         "
665            "# via -c constraints.txt, small-fake-with-deps\n",
666        ),
667        ("--no-annotate", "small-fake-a==0.1\n"),
668    ],
669)
670def test_annotate_option(pip_conf, runner, option, expected):
671    """
672    The output lines has have annotations if option is turned on.
673    """
674    with open("constraints.txt", "w") as constraints_in:
675        constraints_in.write("small-fake-a==0.1")
676    with open("requirements.in", "w") as req_in:
677        req_in.write("-c constraints.txt\n")
678        req_in.write("small_fake_with_deps")
679
680    out = runner.invoke(cli, [option, "-n"])
681
682    assert expected in out.stderr
683    assert out.exit_code == 0
684
685
686@pytest.mark.parametrize(
687    "option, expected",
688    [("--allow-unsafe", "small-fake-a==0.1"), (None, "# small-fake-a")],
689)
690def test_allow_unsafe_option(pip_conf, monkeypatch, runner, option, expected):
691    """
692    Unsafe packages are printed as expected with and without --allow-unsafe.
693    """
694    monkeypatch.setattr("piptools.resolver.UNSAFE_PACKAGES", {"small-fake-a"})
695    with open("requirements.in", "w") as req_in:
696        req_in.write(path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_deps")))
697
698    out = runner.invoke(cli, ["--no-annotate", option] if option else [])
699
700    assert expected in out.stderr.splitlines()
701    assert out.exit_code == 0
702
703
704@pytest.mark.parametrize(
705    "option, attr, expected",
706    [("--cert", "cert", "foo.crt"), ("--client-cert", "client_cert", "bar.pem")],
707)
708@mock.patch("piptools.scripts.compile.parse_requirements")
709def test_cert_option(parse_requirements, runner, option, attr, expected):
710    """
711    The options --cert and --client-crt have to be passed to the PyPIRepository.
712    """
713    with open("requirements.in", "w"):
714        pass
715
716    runner.invoke(cli, [option, expected])
717
718    # Ensure the options in parse_requirements has the expected option
719    assert getattr(parse_requirements.call_args.kwargs["options"], attr) == expected
720
721
722@pytest.mark.parametrize(
723    "option, expected", [("--build-isolation", True), ("--no-build-isolation", False)]
724)
725@mock.patch("piptools.scripts.compile.PyPIRepository")
726@mock.patch("piptools.scripts.compile.parse_requirements")  # prevent to parse
727def test_build_isolation_option(
728    parse_requirements, PyPIRepository, runner, option, expected
729):
730    """
731    A value of the --build-isolation/--no-build-isolation flag
732    must be passed to the PyPIRepository.
733    """
734    with open("requirements.in", "w"):
735        pass
736
737    runner.invoke(cli, [option])
738
739    # Ensure the build_isolation option in PyPIRepository has the expected value.
740    assert PyPIRepository.call_args.kwargs["build_isolation"] is expected
741
742
743@pytest.mark.parametrize(
744    "cli_option, infile_option, expected_package",
745    [
746        # no --pre pip-compile should resolve to the last stable version
747        (False, False, "small-fake-a==0.2"),
748        # pip-compile --pre should resolve to the last pre-released version
749        (True, False, "small-fake-a==0.3b1"),
750        (False, True, "small-fake-a==0.3b1"),
751        (True, True, "small-fake-a==0.3b1"),
752    ],
753)
754def test_pre_option(pip_conf, runner, cli_option, infile_option, expected_package):
755    """
756    Tests pip-compile respects --pre option.
757    """
758    with open("requirements.in", "w") as req_in:
759        if infile_option:
760            req_in.write("--pre\n")
761        req_in.write("small-fake-a\n")
762
763    out = runner.invoke(cli, ["--no-annotate", "-n"] + (["-p"] if cli_option else []))
764
765    assert out.exit_code == 0, out.stderr
766    assert expected_package in out.stderr.splitlines(), out.stderr
767
768
769@pytest.mark.parametrize(
770    "add_options",
771    [
772        [],
773        ["--output-file", "requirements.txt"],
774        ["--upgrade"],
775        ["--upgrade", "--output-file", "requirements.txt"],
776        ["--upgrade-package", "small-fake-a"],
777        ["--upgrade-package", "small-fake-a", "--output-file", "requirements.txt"],
778    ],
779)
780def test_dry_run_option(pip_conf, runner, add_options):
781    """
782    Tests pip-compile doesn't create requirements.txt file on dry-run.
783    """
784    with open("requirements.in", "w") as req_in:
785        req_in.write("small-fake-a\n")
786
787    out = runner.invoke(cli, ["--no-annotate", "--dry-run"] + add_options)
788
789    assert out.exit_code == 0, out.stderr
790    assert "small-fake-a==0.2" in out.stderr.splitlines()
791    assert not os.path.exists("requirements.txt")
792
793
794@pytest.mark.parametrize(
795    "add_options, expected_cli_output_package",
796    [
797        ([], "small-fake-a==0.1"),
798        (["--output-file", "requirements.txt"], "small-fake-a==0.1"),
799        (["--upgrade"], "small-fake-a==0.2"),
800        (["--upgrade", "--output-file", "requirements.txt"], "small-fake-a==0.2"),
801        (["--upgrade-package", "small-fake-a"], "small-fake-a==0.2"),
802        (
803            ["--upgrade-package", "small-fake-a", "--output-file", "requirements.txt"],
804            "small-fake-a==0.2",
805        ),
806    ],
807)
808def test_dry_run_doesnt_touch_output_file(
809    pip_conf, runner, add_options, expected_cli_output_package
810):
811    """
812    Tests pip-compile doesn't touch requirements.txt file on dry-run.
813    """
814    with open("requirements.in", "w") as req_in:
815        req_in.write("small-fake-a\n")
816
817    with open("requirements.txt", "w") as req_txt:
818        req_txt.write("small-fake-a==0.1\n")
819
820    before_compile_mtime = os.stat("requirements.txt").st_mtime
821
822    out = runner.invoke(cli, ["--no-annotate", "--dry-run"] + add_options)
823
824    assert out.exit_code == 0, out.stderr
825    assert expected_cli_output_package in out.stderr.splitlines()
826
827    # The package version must NOT be updated in the output file
828    with open("requirements.txt", "r") as req_txt:
829        assert "small-fake-a==0.1" in req_txt.read().splitlines()
830
831    # The output file must not be touched
832    after_compile_mtime = os.stat("requirements.txt").st_mtime
833    assert after_compile_mtime == before_compile_mtime
834
835
836@pytest.mark.parametrize(
837    "empty_input_pkg, prior_output_pkg",
838    [
839        ("", ""),
840        ("", "small-fake-a==0.1\n"),
841        ("# Nothing to see here", ""),
842        ("# Nothing to see here", "small-fake-a==0.1\n"),
843    ],
844)
845def test_empty_input_file_no_header(runner, empty_input_pkg, prior_output_pkg):
846    """
847    Tests pip-compile creates an empty requirements.txt file,
848    given --no-header and empty requirements.in
849    """
850    with open("requirements.in", "w") as req_in:
851        req_in.write(empty_input_pkg)  # empty input file
852
853    with open("requirements.txt", "w") as req_txt:
854        req_txt.write(prior_output_pkg)
855
856    runner.invoke(cli, ["--no-header", "requirements.in"])
857
858    with open("requirements.txt", "r") as req_txt:
859        assert req_txt.read().strip() == ""
860
861
862def test_upgrade_package_doesnt_remove_annotation(pip_conf, runner):
863    """
864    Tests pip-compile --upgrade-package shouldn't remove "via" annotation.
865    See: GH-929
866    """
867    with open("requirements.in", "w") as req_in:
868        req_in.write("small-fake-with-deps\n")
869
870    runner.invoke(cli)
871
872    # Downgrade small-fake-a to 0.1
873    with open("requirements.txt", "w") as req_txt:
874        req_txt.write(
875            "small-fake-with-deps==0.1\n"
876            "small-fake-a==0.1         # via small-fake-with-deps\n"
877        )
878
879    runner.invoke(cli, ["-P", "small-fake-a"])
880    with open("requirements.txt", "r") as req_txt:
881        assert (
882            "small-fake-a==0.1         # via small-fake-with-deps"
883            in req_txt.read().splitlines()
884        )
885
886
887@pytest.mark.parametrize(
888    "options",
889    [
890        # TODO add --no-index support in OutputWriter
891        # "--no-index",
892        "--index-url https://example.com",
893        "--extra-index-url https://example.com",
894        "--find-links ./libs1",
895        "--trusted-host example.com",
896        "--no-binary :all:",
897        "--only-binary :all:",
898    ],
899)
900def test_options_in_requirements_file(runner, options):
901    """
902    Test the options from requirements.in is copied to requirements.txt.
903    """
904    with open("requirements.in", "w") as reqs_in:
905        reqs_in.write(options)
906
907    out = runner.invoke(cli)
908    assert out.exit_code == 0, out
909
910    with open("requirements.txt") as reqs_txt:
911        assert options in reqs_txt.read().splitlines()
912
913
914@pytest.mark.parametrize(
915    "cli_options, expected_message",
916    (
917        pytest.param(
918            ["--index-url", "file:foo"],
919            "Was file:foo reachable?",
920            id="single index url",
921        ),
922        pytest.param(
923            ["--index-url", "file:foo", "--extra-index-url", "file:bar"],
924            "Were file:foo or file:bar reachable?",
925            id="multiple index urls",
926        ),
927    ),
928)
929def test_unreachable_index_urls(runner, cli_options, expected_message):
930    """
931    Test pip-compile raises an error if index URLs are not reachable.
932    """
933    with open("requirements.in", "w") as reqs_in:
934        reqs_in.write("some-package")
935
936    out = runner.invoke(cli, cli_options)
937
938    assert out.exit_code == 2, out
939
940    stderr_lines = out.stderr.splitlines()
941    assert "No versions found" in stderr_lines
942    assert expected_message in stderr_lines
943
944
945@pytest.mark.parametrize(
946    "current_package, upgraded_package",
947    (
948        pytest.param("small-fake-b==0.1", "small-fake-b==0.2", id="upgrade"),
949        pytest.param("small-fake-b==0.2", "small-fake-b==0.1", id="downgrade"),
950    ),
951)
952def test_upgrade_packages_option_subdependency(
953    pip_conf, runner, current_package, upgraded_package
954):
955    """
956    Test that pip-compile --upgrade-package/-P upgrades/dpwngrades subdependencies.
957    """
958
959    with open("requirements.in", "w") as reqs:
960        reqs.write("small-fake-with-unpinned-deps\n")
961
962    with open("requirements.txt", "w") as reqs:
963        reqs.write("small-fake-a==0.1\n")
964        reqs.write(current_package + "\n")
965        reqs.write("small-fake-with-unpinned-deps==0.1\n")
966
967    out = runner.invoke(
968        cli, ["--no-annotate", "--dry-run", "--upgrade-package", upgraded_package]
969    )
970
971    stderr_lines = out.stderr.splitlines()
972    assert "small-fake-a==0.1" in stderr_lines, "small-fake-a must keep its version"
973    assert (
974        upgraded_package in stderr_lines
975    ), "{} must be upgraded/downgraded to {}".format(current_package, upgraded_package)
976
977
978@pytest.mark.parametrize(
979    "input_opts, output_opts",
980    (
981        # Test that input options overwrite output options
982        pytest.param(
983            "--index-url https://index-url",
984            "--index-url https://another-index-url",
985            id="index url",
986        ),
987        pytest.param(
988            "--extra-index-url https://extra-index-url",
989            "--extra-index-url https://another-extra-index-url",
990            id="extra index url",
991        ),
992        pytest.param("--find-links dir", "--find-links another-dir", id="find links"),
993        pytest.param(
994            "--trusted-host hostname",
995            "--trusted-host another-hostname",
996            id="trusted host",
997        ),
998        pytest.param(
999            "--no-binary :package:", "--no-binary :another-package:", id="no binary"
1000        ),
1001        pytest.param(
1002            "--only-binary :package:",
1003            "--only-binary :another-package:",
1004            id="only binary",
1005        ),
1006        # Test misc corner cases
1007        pytest.param("", "--index-url https://index-url", id="empty input options"),
1008        pytest.param(
1009            "--index-url https://index-url",
1010            (
1011                "--index-url https://index-url\n"
1012                "--extra-index-url https://another-extra-index-url"
1013            ),
1014            id="partially matched options",
1015        ),
1016    ),
1017)
1018def test_remove_outdated_options(runner, input_opts, output_opts):
1019    """
1020    Test that the options from the current requirements.txt wouldn't stay
1021    after compile if they were removed from requirements.in file.
1022    """
1023    with open("requirements.in", "w") as req_in:
1024        req_in.write(input_opts)
1025    with open("requirements.txt", "w") as req_txt:
1026        req_txt.write(output_opts)
1027
1028    out = runner.invoke(cli, ["--no-header"])
1029
1030    assert out.exit_code == 0, out
1031    assert out.stderr.strip() == input_opts
1032
1033
1034def test_sub_dependencies_with_constraints(pip_conf, runner):
1035    # Write constraints file
1036    with open("constraints.txt", "w") as constraints_in:
1037        constraints_in.write("small-fake-a==0.1\n")
1038        constraints_in.write("small-fake-b==0.2\n")
1039        constraints_in.write("small-fake-with-unpinned-deps==0.1")
1040
1041    with open("requirements.in", "w") as req_in:
1042        req_in.write("-c constraints.txt\n")
1043        req_in.write("small_fake_with_deps_and_sub_deps")  # require fake package
1044
1045    out = runner.invoke(cli, ["--no-annotate"])
1046
1047    assert out.exit_code == 0
1048
1049    req_out_lines = set(out.stderr.splitlines())
1050    assert {
1051        "small-fake-a==0.1",
1052        "small-fake-b==0.2",
1053        "small-fake-with-deps-and-sub-deps==0.1",
1054        "small-fake-with-unpinned-deps==0.1",
1055    }.issubset(req_out_lines)
1056