1import argparse
2import os
3
4import pytest
5
6import flynt
7from flynt.cli import run_flynt_cli
8from flynt.cli_messages import farewell_message
9
10
11class ArgumentParser():
12    """
13    Mock class for argparse.ArgumentParser
14
15    Parameters:
16        shadow_parser: a real argparse.ArgumentParser
17        parse_args_return_value: the Namespace that should be returned from parse_args.
18    """
19
20    def __init__(
21        self,
22        shadow_parser: argparse.ArgumentParser,
23        parse_args_return_value: argparse.Namespace,
24        *args,
25        **kwargs,
26    ):
27        self.parse_args_return_value = parse_args_return_value
28        self.shadow_parser = shadow_parser
29
30    def add_mutually_exclusive_group(self):
31        return MutuallyExclusiveGroup(self)
32
33    def add_argument(self, *args, **kwargs):
34        arg = self.shadow_parser.add_argument(*args, **kwargs)
35        if arg.dest not in self.parse_args_return_value:
36            setattr(self.parse_args_return_value, arg.dest, arg.default)
37        return arg
38
39    def print_usage(self):
40        return self.shadow_parser.print_usage()
41
42    def parse_args(self):
43        return self.parse_args_return_value
44
45
46class MutuallyExclusiveGroup(object):
47    """
48    Mock class for argparse.MutuallyExclusiveGroup
49    """
50
51    def __init__(self, parent):
52        self.parent = parent
53
54    def add_argument(self, *args, **kwargs):
55        return self.parent.add_argument(*args, **kwargs)
56
57
58def run_cli_test(monkeypatch, **kwargs):
59    """
60    Runs the CLI, setting arguments according to **kwargs.
61    For example, running with version=True is like passing the --version argument to the CLI.
62    """
63    shadow_parser = argparse.ArgumentParser()
64    parse_args_return_value = argparse.Namespace(**kwargs)
65
66    def argument_parser_mock(*args, **kwargs):
67        return ArgumentParser(shadow_parser, parse_args_return_value, *args, **kwargs)
68
69    monkeypatch.setattr(argparse, "ArgumentParser", argument_parser_mock)
70    return run_flynt_cli()
71
72
73def test_cli_no_args(monkeypatch, capsys):
74    """
75    With no arguments, it should require src to be set
76    """
77    return_code = run_cli_test(monkeypatch)
78    assert return_code == 1
79
80    out, err = capsys.readouterr()
81    assert "the following arguments are required: src" in out
82
83
84def test_cli_version(monkeypatch, capsys):
85    """
86    With --version set, it should only print the version
87    """
88    return_code = run_cli_test(monkeypatch, version=True)
89    assert return_code == 0
90
91    out, err = capsys.readouterr()
92    assert out == f"{flynt.__version__}\n"
93    assert err == ""
94
95
96# Code snippets for testing the -s/--string argument
97cli_string_snippets = pytest.mark.parametrize(
98    "code_in, code_out",
99    [
100        ("'{}'.format(x) + '{}'.format(y)", "f'{x}' + f'{y}'"),
101        (
102            "['{}={}'.format(key, value) for key, value in x.items()]",
103            "[f'{key}={value}' for key, value in x.items()]",
104        ),
105        (
106            '["{}={}".format(key, value) for key, value in x.items()]',
107            '[f"{key}={value}" for key, value in x.items()]',
108        ),
109        (
110            "This ! isn't <> valid .. Python $ code",
111            "This ! isn't <> valid .. Python $ code",
112        ),
113    ],
114)
115
116
117@cli_string_snippets
118def test_cli_string_quoted(monkeypatch, capsys, code_in, code_out):
119    """
120    Tests an invocation with quotes, like:
121
122        flynt -s "some code snippet"
123
124    Then the src argument will be ["some code snippet"].
125    """
126    return_code = run_cli_test(monkeypatch, string=True, src=[code_in])
127    assert return_code == 0
128
129    out, err = capsys.readouterr()
130    assert out.strip() == code_out
131    assert err == ""
132
133
134@cli_string_snippets
135def test_cli_string_unquoted(monkeypatch, capsys, code_in, code_out):
136    """
137    Tests an invocation with no quotes, like:
138
139        flynt -s some code snippet
140
141    Then the src argument will be ["some", "code", "snippet"].
142    """
143    return_code = run_cli_test(monkeypatch, string=True, src=code_in.split())
144    assert return_code == 0
145
146    out, err = capsys.readouterr()
147    assert out.strip() == code_out
148    assert err == ""
149
150
151@pytest.mark.parametrize(
152    "sample_file",
153    ["all_named.py", "first_string.py", "percent_dict.py", "multiline_limit.py"],
154)
155def test_cli_dry_run(monkeypatch, capsys, sample_file):
156    """
157    Tests the --dry-run option with a few files, all changed lines should be shown in the diff
158    """
159    # Get input/output paths and read them
160    folder = os.path.dirname(__file__)
161    source_path = os.path.join(folder, "samples_in", sample_file)
162    expected_path = os.path.join(folder, "expected_out", sample_file)
163    with open(source_path) as file:
164        source_lines = file.readlines()
165    with open(expected_path) as file:
166        converted_lines = file.readlines()
167
168    # Run the CLI
169    return_code = run_cli_test(monkeypatch, dry_run=True, src=[source_path])
170    assert return_code == 0
171
172    # Check that the output includes all changed lines, and the farewell message
173    out, err = capsys.readouterr()
174
175    for line in source_lines:
176        if line not in converted_lines:
177            assert f"-{line}" in out, "Original source line missing from output"
178    for line in converted_lines:
179        if line not in source_lines:
180            assert f"+{line}" in out, "Converted source line missing from output"
181
182    assert out.strip().endswith(farewell_message.strip())
183    assert err == ""
184