|
| 1 | +"""Unit tests for run_ctsm_py_tests module""" |
| 2 | + |
| 3 | +import pytest |
| 4 | +from ctsm.run_ctsm_py_tests import get_pytest_help_item |
| 5 | + |
| 6 | + |
| 7 | +@pytest.fixture(name="sample_pytest_help") |
| 8 | +def fixture_sample_pytest_help(): |
| 9 | + """Sample pytest help text for testing""" |
| 10 | + return """usage: pytest [options] [file_or_dir] [file_or_dir] [...] |
| 11 | +
|
| 12 | +positional arguments: |
| 13 | + file_or_dir |
| 14 | +
|
| 15 | +general: |
| 16 | + -k EXPRESSION Only run tests which match the given substring expression. An expression is a Python evaluable expression where all names are substring-matched against test names and their |
| 17 | + parent classes. Example: -k 'test_method or test_other' matches all test functions and classes whose name contains 'test_method' or 'test_other', while -k 'not test_method' |
| 18 | + matches those that don't contain 'test_method' in their names. -k 'not test_method and not test_other' will eliminate the matches. Additionally keywords are matched to classes |
| 19 | + and functions containing extra names in their 'extra_keyword_matches' set, as well as functions which have names assigned directly to them. The matching is case-insensitive. |
| 20 | + -m MARKEXPR Only run tests matching given mark expression. For example: -m 'mark1 and not mark2'. |
| 21 | + --markers show markers (builtin, plugin and per-project ones). |
| 22 | + -x, --exitfirst Exit instantly on first error or failed test |
| 23 | + --fixtures, --funcargs |
| 24 | + Show available fixtures, sorted by plugin appearance (fixtures with leading '_' are only shown with '-v') |
| 25 | + --fixtures-per-test Show fixtures per test |
| 26 | + --pdbcls=modulename:classname |
| 27 | + Specify a custom interactive Python debugger for use with --pdb.For example: --pdbcls=IPython.terminal.debugger:TerminalPdb |
| 28 | + --pdb Start the interactive Python debugger on errors or KeyboardInterrupt |
| 29 | + --trace Immediately break when running each test |
| 30 | + --capture=method Per-test capturing method: one of fd|sys|no|tee-sys |
| 31 | + -s Shortcut for --capture=no |
| 32 | + --runxfail Report the results of xfail tests as if they were not marked |
| 33 | + --lf, --last-failed Rerun only the tests that failed at the last run (or all if none failed) |
| 34 | + --ff, --failed-first Run all tests, but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown. |
| 35 | + --nf, --new-first Run tests from new files first, then the rest of the tests sorted by file mtime |
| 36 | + --cache-show=[CACHESHOW] |
| 37 | + Show cache contents, don't perform collection or tests. Optional argument: glob (default: '*'). |
| 38 | + --cache-clear Remove all cache contents at start of test run |
| 39 | + --lfnf, --last-failed-no-failures={all,none} |
| 40 | + With ``--lf``, determines whether to execute tests when there are no previously (known) failures or when no cached ``lastfailed`` data was found. ``all`` (the default) runs the |
| 41 | + full test suite again. ``none`` just emits a message about no known failures and exits successfully. |
| 42 | + --sw, --stepwise Exit on test failure and continue from last failing test next time |
| 43 | + --sw-skip, --stepwise-skip |
| 44 | + Ignore the first failing test but stop on the next failing test. Implicitly enables --stepwise. |
| 45 | +""" |
| 46 | + |
| 47 | + |
| 48 | +class TestGetPytestHelpItem: |
| 49 | + """Tests for get_pytest_help_item function""" |
| 50 | + |
| 51 | + def test_extract_short_option(self, sample_pytest_help): |
| 52 | + """Test extracting a short option with description""" |
| 53 | + result = get_pytest_help_item(sample_pytest_help, "-k") |
| 54 | + assert result.startswith("-k EXPRESSION") |
| 55 | + assert "Only run tests which match the given substring expression" in result |
| 56 | + assert "case-insensitive" in result |
| 57 | + |
| 58 | + def test_extract_long_option(self, sample_pytest_help): |
| 59 | + """Test extracting a long option with description""" |
| 60 | + result = get_pytest_help_item(sample_pytest_help, "--markers") |
| 61 | + assert result.startswith("--markers") |
| 62 | + assert "show markers (builtin, plugin and per-project ones)" in result |
| 63 | + |
| 64 | + def test_extract_option_with_short_and_long(self, sample_pytest_help): |
| 65 | + """Test extracting an option that has both short and long forms""" |
| 66 | + result = get_pytest_help_item(sample_pytest_help, "-x") |
| 67 | + assert result.startswith("-x, --exitfirst") |
| 68 | + assert "Exit instantly on first error or failed test" in result |
| 69 | + print(result) |
| 70 | + |
| 71 | + def test_extract_option_with_argument(self, sample_pytest_help): |
| 72 | + """Test extracting an option that takes an argument""" |
| 73 | + result = get_pytest_help_item(sample_pytest_help, "--pdbcls") |
| 74 | + assert result.startswith("--pdbcls=modulename:classname") |
| 75 | + assert "custom interactive Python debugger" in result |
| 76 | + |
| 77 | + def test_extract_multiline_description(self, sample_pytest_help): |
| 78 | + """Test that multiline descriptions are collapsed into single line""" |
| 79 | + result = get_pytest_help_item(sample_pytest_help, "--fixtures") |
| 80 | + assert "\n" not in result |
| 81 | + assert "Show available fixtures" in result |
| 82 | + assert "sorted by plugin appearance" in result |
| 83 | + |
| 84 | + def test_extract_option_with_optional_argument(self, sample_pytest_help): |
| 85 | + """Test extracting an option with optional argument""" |
| 86 | + result = get_pytest_help_item(sample_pytest_help, "--cache-show") |
| 87 | + assert result.startswith("--cache-show=[CACHESHOW]") |
| 88 | + assert "Show cache contents" in result |
| 89 | + assert "glob (default: '*')" in result |
| 90 | + |
| 91 | + def test_extract_option_with_choices(self, sample_pytest_help): |
| 92 | + """Test extracting an option with choices""" |
| 93 | + result = get_pytest_help_item(sample_pytest_help, "--lfnf") |
| 94 | + assert result.startswith("--lfnf, --last-failed-no-failures={all,none}") |
| 95 | + assert "With ``--lf``" in result |
| 96 | + |
| 97 | + def test_option_not_found_raises_error(self, sample_pytest_help): |
| 98 | + """Test that RuntimeError is raised when option is not found""" |
| 99 | + with pytest.raises(RuntimeError, match="Failed to get pytest help for --nonexistent"): |
| 100 | + get_pytest_help_item(sample_pytest_help, "--nonexistent") |
| 101 | + |
| 102 | + def test_only_matches_line_start(self, sample_pytest_help): |
| 103 | + """Test that option must be at start of line (after whitespace)""" |
| 104 | + # "pdb" appears in both "--pdb" and "--pdbcls", but searching for "pdb" |
| 105 | + # should not match "--pdbcls" since "pdb" is not at the start |
| 106 | + result = get_pytest_help_item(sample_pytest_help, "--pdb") |
| 107 | + assert result.startswith("--pdb") |
| 108 | + assert "Start the interactive Python debugger" in result |
| 109 | + # Should not contain pdbcls content |
| 110 | + assert "modulename:classname" not in result |
| 111 | + |
| 112 | + def test_handles_indented_options(self): |
| 113 | + """Test that function handles options with leading whitespace""" |
| 114 | + help_text = """ |
| 115 | +general: |
| 116 | + --foo This is foo option |
| 117 | + with multiple lines |
| 118 | + --bar This is bar option |
| 119 | +""" |
| 120 | + result = get_pytest_help_item(help_text, "--foo") |
| 121 | + assert result.startswith("--foo") |
| 122 | + assert "This is foo option" in result |
| 123 | + assert "with multiple lines" in result |
| 124 | + |
| 125 | + def test_stops_at_blank_line(self): |
| 126 | + """Test that description extraction stops at blank line""" |
| 127 | + help_text = """ |
| 128 | + --option First line |
| 129 | + Second line |
| 130 | +
|
| 131 | + --next Next option |
| 132 | +""" |
| 133 | + result = get_pytest_help_item(help_text, "--option") |
| 134 | + assert "First line" in result |
| 135 | + assert "Second line" in result |
| 136 | + assert "Next option" not in result |
| 137 | + |
| 138 | + def test_stops_at_less_indented_line(self): |
| 139 | + """Test that description extraction stops when indentation decreases""" |
| 140 | + help_text = """ |
| 141 | + --option First line |
| 142 | + Second line |
| 143 | + --next Next option |
| 144 | +""" |
| 145 | + result = get_pytest_help_item(help_text, "--option") |
| 146 | + assert "First line" in result |
| 147 | + assert "Second line" in result |
| 148 | + assert "Next option" not in result |
| 149 | + |
| 150 | + def test_colon_separator_in_output(self, sample_pytest_help): |
| 151 | + """Test that output contains colon separator between header and description""" |
| 152 | + result = get_pytest_help_item(sample_pytest_help, "-s") |
| 153 | + assert ":" in result |
| 154 | + # The format includes the full header line, then colon, then description |
| 155 | + assert result.startswith("-s") |
| 156 | + assert "Shortcut for --capture=no" in result |
| 157 | + |
| 158 | + def test_empty_help_text(self): |
| 159 | + """Test behavior with empty help text""" |
| 160 | + with pytest.raises(RuntimeError, match="Failed to get pytest help for --option"): |
| 161 | + get_pytest_help_item("", "--option") |
| 162 | + |
| 163 | + def test_option_with_no_description(self): |
| 164 | + """Test option that has no description lines""" |
| 165 | + help_text = """ |
| 166 | + --option |
| 167 | +
|
| 168 | + --next Next option |
| 169 | +""" |
| 170 | + result = get_pytest_help_item(help_text, "--option") |
| 171 | + # Should return just the header with empty description |
| 172 | + assert result.startswith("--option:") |
0 commit comments