File size: 13,241 Bytes
c1f3d83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d155aa
c1f3d83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87ca90f
 
 
 
 
 
c1f3d83
87ca90f
c1f3d83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292016c
 
f02c614
 
 
 
 
 
 
292016c
 
 
f02c614
 
 
 
 
 
 
 
 
 
 
 
292016c
 
 
 
 
 
 
 
3432acf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf3d32
 
 
 
292016c
cdf3d32
 
 
 
 
 
292016c
cdf3d32
 
f02c614
 
 
cdf3d32
 
 
 
 
 
 
 
 
 
 
 
 
f02c614
 
292016c
92a43b0
 
 
 
 
 
 
 
 
 
 
292016c
 
 
 
f02c614
292016c
 
 
 
 
223d4f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
"""The exit-code contract, and the banner that should appear exactly once.

Exit codes are three-valued on purpose: 0 worked, 1 the command ran and
reported findings, 2 it was called wrong. Before that split, `nexa ask` with no
question printed help and returned 0 -- so `nexa ask odoo "$Q" || exit 1`
reported success for an empty $Q. Every row of the table below is a contract,
and the comments mark the ones that used to return the wrong number.

Most of these call main() in-process: it returns the code rather than exiting,
so the assertion is direct, it runs in milliseconds, and a coverage run can
actually see the branches being taken. A handful go through a real subprocess at
the bottom, because in-process calls would never catch a broken __main__ block.
"""
import os
import subprocess
import sys

import pytest

import nexa

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OK, FINDINGS, MISUSE = 0, 1, 2


@pytest.fixture(autouse=True)
def fresh_banner(monkeypatch):
    """banner(once=True) is process-global, and every test here is a new run."""
    monkeypatch.setattr(nexa, "_BANNER_SHOWN", False)


@pytest.fixture(autouse=True)
def in_repo(monkeypatch):
    """nexa resolves the corpus relative to its own file, but check and index
    take paths from the caller's working directory."""
    monkeypatch.chdir(ROOT)


def main(*args):
    return nexa.main(list(args))


def shell(*args):
    proc = subprocess.run([sys.executable, os.path.join(ROOT, "nexa.py")] + list(args),
                          capture_output=True, text=True, encoding="utf-8",
                          errors="replace", cwd=ROOT)
    return proc.returncode, (proc.stdout or "")


CASES = [
    ([], OK, "bare invocation prints the command list"),
    (["version"], OK, "machine-readable, and the one command with no banner"),
    (["--version"], OK, "the flag spelling"),
    (["help"], OK, "help with no topic is the command list"),
    (["help", "ask"], OK, "help for a real command"),
    (["help", "bogus"], MISUSE, "was 1: an unknown topic is misuse, not a finding"),
    (["bogus"], MISUSE, "was 1"),
    (["ask"], MISUSE, "was 0 -- the bug that motivated the split"),
    (["ask", "odoo"], MISUSE, "was 0: a stack with no question is still misuse"),
    (["run"], MISUSE, "was 0"),
    (["run", "odoo"], MISUSE, "was 0"),
    (["index"], MISUSE, "was 0"),
    (["check"], MISUSE, "was 0"),
    (["check", "no_such_file_here.py"], MISUSE, "was 1: a bad path is a bad argument"),
    (["rules"], OK, "the whole corpus"),
    (["rules", "onchange"], OK, "a term that matches"),
    (["rules", "zzzznomatch"], FINDINGS, "ran fine, found nothing -- NOT misuse"),
    (["rules", "--stack", "odoo"], OK, "the one stack, named"),
    (["rules", "--full", "odoo.onchange-is-not-validation"], OK, "a real id"),
    (["rules", "--full", "no.such.rule"], MISUSE, "was 1: an id that does not exist"),
    (["validate"], OK, "corpus is schema-valid"),
    (["crosscheck"], OK, "informational, never fails the build"),
    (["coverage"], OK, "reports its own blind spots"),
    (["adversarial"], OK, "all three probes"),
    (["adversarial", "evasion"], OK, "one probe by name"),
]


@pytest.mark.parametrize("args,expected,why", CASES,
                         ids=[" ".join(c[0]) or "(none)" for c in CASES])
def test_exit_code(args, expected, why, capsys):
    assert main(*args) == expected, "{0}: {1}".format(" ".join(args) or "(none)", why)


def test_empty_search_and_bad_id_are_different_codes(capsys):
    """The distinction the three-valued scheme exists for: 'I looked and found
    nothing' is a result; 'that id does not exist' is a mistake."""
    assert main("rules", "zzzznomatch") == FINDINGS
    assert main("rules", "--full", "no.such.rule") == MISUSE


def test_usage_error_still_shows_the_help_text(capsys):
    """Exiting non-zero is no reason to withhold the thing the user needs.

    Asserted against the usage string the command actually declares, rather
    than a copy of it: this used to spell out the stack list and went red when
    a fourth stack was added, which is not what it is testing.
    """
    assert main("ask") == MISUSE
    assert nexa.COMMANDS["ask"][1] in capsys.readouterr().out


def test_unknown_command_points_back_at_the_command_list(capsys):
    for args in (["bogus"], ["help", "bogus"]):
        nexa._BANNER_SHOWN = False
        main(*args)
        out = capsys.readouterr().out
        assert "unknown command" in out
        assert "for the command list" in out, "both spellings give the same hint"


def test_commands_table_and_dispatch_agree(capsys):
    """A command listed in help with no dispatch branch would print its help and
    then fall through to 'unknown command'."""
    for name in nexa.COMMANDS:
        nexa._BANNER_SHOWN = False
        assert main("help", name) == OK
        assert "unknown command" not in capsys.readouterr().out


# ---------------------------------------------------------------- the banner

def test_banner_prints_once_per_process(capsys):
    nexa.banner(subtitle=False, once=True)
    nexa.banner(subtitle=False, once=True)
    assert capsys.readouterr().out.count("NEXA") == 1


def test_banner_without_once_always_prints(capsys):
    nexa.banner(subtitle=False)
    nexa.banner(subtitle=False)
    assert capsys.readouterr().out.count("NEXA") == 2


def test_every_command_wears_the_banner(capsys):
    for args in (["rules"], ["validate"], ["coverage"], ["adversarial"], ["bogus"]):
        nexa._BANNER_SHOWN = False
        main(*args)
        assert "NEXA" in capsys.readouterr().out, "{0} printed no banner".format(args)


def test_version_is_the_one_command_without_a_banner(capsys):
    main("version")
    out = capsys.readouterr().out
    assert out.strip() == "nexa {0}".format(nexa.VERSION)


# ------------------------------------------------------ the real entry point

def test_subprocess_entry_point_exits_with_the_returned_code():
    """In-process tests never execute the __main__ block, so sys.exit(main())
    and the KeyboardInterrupt handler around it would go unchecked."""
    assert shell("version")[0] == OK
    assert shell("bogus")[0] == MISUSE
    assert shell("ask")[0] == MISUSE


def test_subprocess_version_is_parseable():
    code, out = shell("version")
    assert code == OK
    assert out.strip() == "nexa {0}".format(nexa.VERSION)


def test_subprocess_output_is_clean_when_not_a_terminal():
    """Piped output must carry no escape codes: COLOR is gated on isatty, and a
    redirect that captured raw ANSI would make `nexa rules > file` unreadable."""
    code, out = shell("rules", "onchange")
    assert code == OK
    assert "\033" not in out


# --------------------------------------------- the wordmark and its underbar

def _art_rows(art):
    """The art's rows, indentation intact, without the blank lines the triple-
    quoted literal starts and ends with."""
    return [row for row in art.splitlines() if row.strip()]


def test_both_spellings_are_the_same_height():
    """banner() bands by row index, so a fallback of a different height would
    paint the bar as a letterform and vice versa."""
    assert len(_art_rows(nexa.BANNER_UNICODE)) == len(_art_rows(nexa.BANNER_ASCII))
    # The wordmark, and the bar under it.
    assert len(_art_rows(nexa.BANNER_UNICODE)) == nexa.BANNER_ROWS + 1


def test_the_bar_is_as_wide_as_the_wordmark():
    """The bar is light thrown by the sign. One that overhangs the letters, or
    falls short of them, reads as a rule somebody drew underneath instead."""
    for art in (nexa.BANNER_UNICODE, nexa.BANNER_ASCII):
        widths = set(len(row) for row in _art_rows(art))
        assert len(widths) == 1, art


def test_the_ascii_fallback_is_actually_ascii():
    """It exists for a console on a legacy code page. A block-drawing character
    surviving into it defeats the entire point."""
    assert nexa.BANNER_ASCII.isascii()


def test_glyphs_lists_every_non_ascii_character_the_module_can_print():
    """GLYPHS is what _enable_utf8 probes a declared encoding with.

    A glyph added to the UI and not to GLYPHS makes the probe pass on an
    encoding that cannot hold it, and the character reaches the terminal
    anyway -- the exact crash the fallback exists to prevent, reintroduced one
    character at a time. Asserted against the source rather than against the
    constants, because those are already degraded to ASCII when UNICODE is
    False and would have nothing to say in the run that matters.

    Comments are excluded. They are never printed, and the ones describing the
    bar name the very characters this is looking for.
    """
    import io
    import tokenize
    with open(os.path.join(ROOT, "nexa.py"), encoding="utf-8") as handle:
        source = handle.read()
    printable = set()
    for token in tokenize.generate_tokens(io.StringIO(source).readline):
        if token.type == tokenize.COMMENT:
            continue
        printable.update(ch for ch in token.string if ord(ch) > 127)
    assert printable, "the scan found nothing, so it is not reading the art"
    missing = sorted(printable - set(nexa.GLYPHS))
    assert not missing, "used but not in nexa.GLYPHS: " + " ".join(missing)


def _cube(n):
    """An xterm-256 cube index as its (r, g, b) coordinates, each 0-5."""
    n -= 16
    return (n // 36, (n % 36) // 6, n % 6)


def test_the_ramp_sweeps_cyan_to_magenta():
    """Endpoints, asserted against the NUMBERS rather than the escape codes.

    Under NO_COLOR every code is the empty string, so a test written against
    codes would pass for any ramp at all, which is worse than not testing it.
    """
    assert nexa.BANNER_RAMP[0] == 51, "starts on cyan"
    assert nexa.BANNER_RAMP[-1] == 201, "ends on magenta"
    assert nexa.BANNER_BAR == 214, "amber, what the rest of the CLI warns in"
    assert nexa.BANNER_BAR not in nexa.BANNER_RAMP, (
        "the bar contrasts with the wordmark rather than continuing it")


def test_the_steps_between_are_an_interpolation_not_a_shortlist():
    """The whole claim of a ramp. Cyan to magenta the short way round holds
    blue at maximum while red rises and green falls; a colour picked because
    somebody liked it breaks one of those three, and the gradient develops a
    kink no amount of squinting at a screenshot reliably catches."""
    steps = [_cube(n) for n in nexa.BANNER_RAMP]
    assert all(b == 5 for _r, _g, b in steps), steps
    reds = [r for r, _g, _b in steps]
    greens = [g for _r, g, _b in steps]
    assert reds == sorted(reds), "red must rise monotonically"
    assert greens == sorted(greens, reverse=True), "green must fall monotonically"
    assert len(set(nexa.BANNER_RAMP)) == len(nexa.BANNER_RAMP), (
        "two rows sharing a colour is a ramp with a step missing")

def test_the_readme_shows_the_banner_that_ships():
    """The README drifted silently once already -- it was still showing an
    outlined NX after the art had changed twice, and that stale block reached
    two public remotes before anybody noticed. A wordmark in the README is a
    claim about what the command prints, so it is the same art or it is wrong."""
    path = os.path.join(ROOT, "README.md")
    with open(path, encoding="utf-8") as handle:
        readme = handle.read()
    for row in _art_rows(nexa.BANNER_UNICODE):
        assert row in readme, "README is missing this row of the wordmark: " + row

def test_the_rendered_palette_is_built_from_the_ramp():
    """And the codes that reach the terminal come from those numbers rather than
    from a second, hand-kept list that can drift from them."""
    assert nexa.WORDMARK_GRADIENT == [nexa.c(n) for n in nexa.BANNER_RAMP]
    assert nexa.BAR_COLOUR == nexa.c(nexa.BANNER_BAR)

def test_the_wordmark_row_count_matches_the_hue_count():
    """One hue per letterform row. If they drift, the banding wraps and two rows
    share a colour."""
    assert len(nexa.WORDMARK_GRADIENT) == nexa.BANNER_ROWS


def test_the_banner_prints_once_in_a_real_run():
    """`python nexa.py` leaves the name `nexa` unclaimed, so nexa_cmds doing
    `import nexa` loaded a SECOND copy of the module -- with its own COLOR, its
    own UNICODE and its own _BANNER_SHOWN. banner(once=True) then printed twice
    on `doctor` and `build`, each copy believing it was the first.

    Only a subprocess can see this. Under pytest the module is imported once
    under its own name, the two never diverge, and the test above passes while
    the CLI prints two banners.
    """
    _, out = shell("doctor")
    assert out.count("validation intelligence") == 1, out


def test_the_banner_says_whose_work_it_is(capsys):
    """The byline rides under the wordmark on every command that prints one,
    including the short form. Read from the same two constants the README
    footer and LICENSE spell out, so the three cannot drift apart."""
    nexa.banner(subtitle=False)
    out = capsys.readouterr().out
    assert "created by " + nexa.AUTHOR in out
    assert nexa.YEAR in out


def test_version_stays_bare(capsys):
    """The byline must not reach the one line a script parses."""
    assert nexa.main(["version"]) == 0
    assert capsys.readouterr().out.strip() == "nexa " + nexa.VERSION