summaryrefslogtreecommitdiffstats
path: root/test/units/plugins/shell/test_powershell.py
blob: 40208695496ce5ceb2450adf1413a21bf54727a3 (plain)
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
from __future__ import annotations

import pytest

from ansible.plugins.shell.powershell import _parse_clixml, _replace_stderr_clixml, ShellModule

CLIXML_WITH_ERROR = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
    b'<S S="Error">My error</S></Objs>'


def test_replace_stderr_clixml_by_itself():
    data = CLIXML_WITH_ERROR
    expected = b"My error"
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_with_pre_and_post_lines():
    data = b"pre\r\n" + CLIXML_WITH_ERROR + b"\r\npost"
    expected = b"pre\r\nMy error\r\npost"
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_with_remaining_data_on_line():
    data = b"pre\r\n" + CLIXML_WITH_ERROR + b"inline\r\npost"
    expected = b"pre\r\nMy errorinline\r\npost"
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_with_non_utf8_data():
    # \x82 in cp437 is é but is an invalid UTF-8 sequence
    data = CLIXML_WITH_ERROR.replace(b"error", b"\x82rror")
    expected = "My érror".encode("utf-8")
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_across_liens():
    data = b"#< CLIXML\r\n<Objs Version=\"foo\">\r\n</Objs>"
    expected = data
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_with_invalid_clixml_data():
    data = b"#< CLIXML\r\n<Objs Version=\"foo\"><</Objs>"
    expected = data
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_with_no_clixml():
    data = b"foo"
    expected = data
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_replace_stderr_clixml_with_header_but_no_data():
    data = b"foo\r\n#< CLIXML\r\n"
    expected = data
    actual = _replace_stderr_clixml(data)

    assert actual == expected


def test_parse_clixml_empty():
    empty = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"></Objs>'
    expected = b''
    actual = _parse_clixml(empty)
    assert actual == expected


def test_parse_clixml_with_progress():
    progress = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
               b'<Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS>' \
               b'<I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
               b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj></Objs>'
    expected = b''
    actual = _parse_clixml(progress)
    assert actual == expected


def test_parse_clixml_single_stream():
    single_stream = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
                    b'<S S="Error">fake : The term \'fake\' is not recognized as the name of a cmdlet. Check _x000D__x000A_</S>' \
                    b'<S S="Error">the spelling of the name, or if a path was included._x000D__x000A_</S>' \
                    b'<S S="Error">At line:1 char:1_x000D__x000A_</S>' \
                    b'<S S="Error">+ fake cmdlet_x000D__x000A_</S><S S="Error">+ ~~~~_x000D__x000A_</S>' \
                    b'<S S="Error">    + CategoryInfo          : ObjectNotFound: (fake:String) [], CommandNotFoundException_x000D__x000A_</S>' \
                    b'<S S="Error">    + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_</S>' \
                    b'<S S="Error"> _x000D__x000A_</S>' \
                    b'</Objs>'
    expected = b"fake : The term 'fake' is not recognized as the name of a cmdlet. Check \r\n" \
               b"the spelling of the name, or if a path was included.\r\n" \
               b"At line:1 char:1\r\n" \
               b"+ fake cmdlet\r\n" \
               b"+ ~~~~\r\n" \
               b"    + CategoryInfo          : ObjectNotFound: (fake:String) [], CommandNotFoundException\r\n" \
               b"    + FullyQualifiedErrorId : CommandNotFoundException\r\n" \
               b" \r\n"
    actual = _parse_clixml(single_stream)
    assert actual == expected


def test_parse_clixml_multiple_streams():
    multiple_stream = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
                      b'<S S="Error">fake : The term \'fake\' is not recognized as the name of a cmdlet. Check _x000D__x000A_</S>' \
                      b'<S S="Error">the spelling of the name, or if a path was included._x000D__x000A_</S>' \
                      b'<S S="Error">At line:1 char:1_x000D__x000A_</S>' \
                      b'<S S="Error">+ fake cmdlet_x000D__x000A_</S><S S="Error">+ ~~~~_x000D__x000A_</S>' \
                      b'<S S="Error">    + CategoryInfo          : ObjectNotFound: (fake:String) [], CommandNotFoundException_x000D__x000A_</S>' \
                      b'<S S="Error">    + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S>' \
                      b'<S S="Info">hi info</S>' \
                      b'<S S="Info">other</S>' \
                      b'</Objs>'
    expected = b"hi infoother"
    actual = _parse_clixml(multiple_stream, stream="Info")
    assert actual == expected


def test_parse_clixml_multiple_elements():
    multiple_elements = b'#< CLIXML\r\n#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
                        b'<Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS>' \
                        b'<I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
                        b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj>' \
                        b'<S S="Error">Error 1</S></Objs>' \
                        b'<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0">' \
                        b'<TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS>' \
                        b'<I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
                        b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj>' \
                        b'<Obj S="progress" RefId="1"><TNRef RefId="0" /><MS><I64 N="SourceId">2</I64>' \
                        b'<PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
                        b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj>' \
                        b'<S S="Error">Error 2</S></Objs>'
    expected = b"Error 1\r\nError 2"
    actual = _parse_clixml(multiple_elements)
    assert actual == expected


@pytest.mark.parametrize('clixml, expected', [
    ('', ''),
    ('just newline _x000A_', 'just newline \n'),
    ('surrogate pair _xD83C__xDFB5_', 'surrogate pair 🎵'),
    ('null char _x0000_', 'null char \0'),
    ('normal char _x0061_', 'normal char a'),
    ('escaped literal _x005F_x005F_', 'escaped literal _x005F_'),
    ('underscope before escape _x005F__x000A_', 'underscope before escape _\n'),
    ('surrogate high _xD83C_', 'surrogate high \uD83C'),
    ('surrogate low _xDFB5_', 'surrogate low \uDFB5'),
    ('lower case hex _x005f_', 'lower case hex _'),
    ('invalid hex _x005G_', 'invalid hex _x005G_'),
    # Tests regex actually matches UTF-16-BE hex chars (b"\x00" then hex char).
    ("_x\u6100\u6200\u6300\u6400_", "_x\u6100\u6200\u6300\u6400_"),
])
def test_parse_clixml_with_comlex_escaped_chars(clixml, expected):
    clixml_data = (
        '<# CLIXML\r\n'
        '<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">'
        f'<S S="Error">{clixml}</S>'
        '</Objs>'
    ).encode()
    b_expected = expected.encode(errors="surrogatepass")

    actual = _parse_clixml(clixml_data)
    assert actual == b_expected


def test_join_path_unc():
    pwsh = ShellModule()
    unc_path_parts = ['\\\\host\\share\\dir1\\\\dir2\\', '\\dir3/dir4', 'dir5', 'dir6\\']
    expected = '\\\\host\\share\\dir1\\dir2\\dir3\\dir4\\dir5\\dir6'
    actual = pwsh.join_path(*unc_path_parts)
    assert actual == expected