microsoft/qdk
Publicmirrored fromhttps://github.com/microsoft/qdkAvailable
source/pip/tests/test_project.py
259lines · modecode
| 1 | # Copyright (c) Microsoft Corporation. |
| 2 | # Licensed under the MIT License. |
| 3 | |
| 4 | import pytest |
| 5 | import os |
| 6 | |
| 7 | |
| 8 | @pytest.fixture |
| 9 | def qsharp(): |
| 10 | import qsharp |
| 11 | import qsharp._fs |
| 12 | import qsharp._http |
| 13 | |
| 14 | qsharp._fs.read_file = read_file_memfs |
| 15 | qsharp._fs.list_directory = list_directory_memfs |
| 16 | qsharp._fs.exists = exists_memfs |
| 17 | qsharp._fs.join = join_memfs |
| 18 | qsharp._fs.resolve = resolve_memfs |
| 19 | qsharp._http.fetch_github = fetch_github_test |
| 20 | |
| 21 | return qsharp |
| 22 | |
| 23 | |
| 24 | def test_project(qsharp) -> None: |
| 25 | qsharp.init(project_root="/good") |
| 26 | result = qsharp.eval("Test.ReturnsFour()") |
| 27 | assert result == 4 |
| 28 | |
| 29 | |
| 30 | def test_project_compile_error(qsharp) -> None: |
| 31 | with pytest.raises(Exception) as excinfo: |
| 32 | qsharp.init(project_root="/compile_error") |
| 33 | assert str(excinfo.value).startswith("Qsc.TypeCk.TyMismatch") |
| 34 | |
| 35 | |
| 36 | def test_project_bad_qsharp_json(qsharp) -> None: |
| 37 | with pytest.raises(Exception) as excinfo: |
| 38 | qsharp.init(project_root="/bad_qsharp_json") |
| 39 | assert str(excinfo.value).find("Failed to parse manifest") != -1 |
| 40 | |
| 41 | |
| 42 | def test_project_unreadable_qsharp_json(qsharp) -> None: |
| 43 | with pytest.raises(Exception) as excinfo: |
| 44 | qsharp.init(project_root="/unreadable_qsharp_json") |
| 45 | assert str(excinfo.value).startswith( |
| 46 | "Error reading /unreadable_qsharp_json/qsharp.json." |
| 47 | ) |
| 48 | |
| 49 | |
| 50 | def test_project_unreadable_source(qsharp) -> None: |
| 51 | with pytest.raises(Exception) as excinfo: |
| 52 | qsharp.init(project_root="/unreadable_source") |
| 53 | # If this seems like a silly substring to assert on, it's |
| 54 | # because the error reporting code is inserting a line break |
| 55 | # between "could not" and "read test.qs" |
| 56 | assert str(excinfo.value).find("OSError: could not") != -1 |
| 57 | |
| 58 | |
| 59 | def test_project_dependencies(qsharp) -> None: |
| 60 | qsharp.init(project_root="/with_deps") |
| 61 | result = qsharp.eval("Test.CallsDependency()") |
| 62 | assert result == 4 |
| 63 | |
| 64 | |
| 65 | def test_project_circular_dependency_error(qsharp) -> None: |
| 66 | with pytest.raises(Exception) as excinfo: |
| 67 | qsharp.init(project_root="/circular") |
| 68 | assert str(excinfo.value).find("Circular dependency detected between") != -1 |
| 69 | |
| 70 | |
| 71 | def test_github_dependency(qsharp) -> None: |
| 72 | qsharp.init(project_root="/with_github_dep") |
| 73 | result = qsharp.eval("Test.CallsDependency()") |
| 74 | assert result == 12 |
| 75 | |
| 76 | |
| 77 | def test_circuit(qsharp) -> None: |
| 78 | qsharp.init(project_root="/circuit") |
| 79 | result = qsharp.eval("Test.TestCircuit()") |
| 80 | assert result == qsharp.Result.Zero |
| 81 | |
| 82 | |
| 83 | with open( |
| 84 | os.path.join(os.path.dirname(__file__), "circuit.qsc"), "r", encoding="utf-8" |
| 85 | ) as f: |
| 86 | circuit_qsc_contents = f.read() |
| 87 | |
| 88 | memfs = { |
| 89 | "": { |
| 90 | "good": { |
| 91 | "src": { |
| 92 | "test.qs": "namespace Test { operation ReturnsFour() : Int { 4 } export ReturnsFour; }", |
| 93 | }, |
| 94 | "qsharp.json": "{}", |
| 95 | }, |
| 96 | "bad_qsharp_json": {"qsharp.json": "BAD_JSON_CONTENTS"}, |
| 97 | "unreadable_qsharp_json": { |
| 98 | "qsharp.json": OSError("could not read qsharp.json") |
| 99 | }, |
| 100 | "unreadable_source": { |
| 101 | "src": { |
| 102 | "test.qs": OSError("could not read test.qs"), |
| 103 | }, |
| 104 | "qsharp.json": "{}", |
| 105 | }, |
| 106 | "compile_error": { |
| 107 | "src": { |
| 108 | "test.qs": "namespace Test { operation ReturnsFour() : Int { 4.0 } }", |
| 109 | }, |
| 110 | "qsharp.json": "{}", |
| 111 | }, |
| 112 | "with_deps": { |
| 113 | "src": { |
| 114 | "test.qs": "namespace Test { operation CallsDependency() : Int { return Foo.Test.ReturnsFour(); } }", |
| 115 | }, |
| 116 | "qsharp.json": """ |
| 117 | { |
| 118 | "dependencies": { |
| 119 | "Foo": { |
| 120 | "path": "../good" |
| 121 | } |
| 122 | } |
| 123 | }""", |
| 124 | }, |
| 125 | "circular": { |
| 126 | "src": { |
| 127 | "test.qs": "namespace Test {}", |
| 128 | }, |
| 129 | "qsharp.json": """ |
| 130 | { |
| 131 | "dependencies": { |
| 132 | "Foo": { |
| 133 | "path": "../circular" |
| 134 | } |
| 135 | } |
| 136 | }""", |
| 137 | }, |
| 138 | "with_github_dep": { |
| 139 | "src": { |
| 140 | "test.qs": "namespace Test { operation CallsDependency() : Int { return Foo.Test.ReturnsTwelve(); } }", |
| 141 | }, |
| 142 | "qsharp.json": """ |
| 143 | { |
| 144 | "dependencies": { |
| 145 | "Foo": { |
| 146 | "github" : { |
| 147 | "owner" : "test-owner", |
| 148 | "repo" : "test-repo", |
| 149 | "ref" : "12345" |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | }""", |
| 154 | }, |
| 155 | "circuit": { |
| 156 | "src": { |
| 157 | "test.qs": "namespace Test {" |
| 158 | " import circuit.circuit;" |
| 159 | " operation TestCircuit() : Result {" |
| 160 | " use qs = Qubit[2];" |
| 161 | " let result = circuit(qs);" |
| 162 | " ResetAll(qs);" |
| 163 | " result" |
| 164 | " }" |
| 165 | "}", |
| 166 | "circuit.qsc": circuit_qsc_contents, |
| 167 | }, |
| 168 | "qsharp.json": "{}", |
| 169 | }, |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | |
| 174 | def fetch_github_test(owner: str, repo: str, ref: str, path: str): |
| 175 | if ( |
| 176 | owner == "test-owner" |
| 177 | and repo == "test-repo" |
| 178 | and ref == "12345" |
| 179 | and path == "/qsharp.json" |
| 180 | ): |
| 181 | return """{ "files" : ["src/test.qs"] }""" |
| 182 | if ( |
| 183 | owner == "test-owner" |
| 184 | and repo == "test-repo" |
| 185 | and ref == "12345" |
| 186 | and path == "/src/test.qs" |
| 187 | ): |
| 188 | return "namespace Test { operation ReturnsTwelve() : Int { 12 } export ReturnsTwelve;}" |
| 189 | raise Exception(f"Unexpected fetch_github call: {owner}, {repo}, {ref}, {path}") |
| 190 | |
| 191 | |
| 192 | def read_file_memfs(path): |
| 193 | global memfs |
| 194 | item = memfs |
| 195 | for part in path.split("/"): |
| 196 | if part in item: |
| 197 | if isinstance(item[part], OSError): |
| 198 | raise item[part] |
| 199 | else: |
| 200 | item = item[part] |
| 201 | else: |
| 202 | raise Exception("File not found: " + path) |
| 203 | |
| 204 | return (path, item) |
| 205 | |
| 206 | |
| 207 | def list_directory_memfs(dir_path): |
| 208 | global memfs |
| 209 | item = memfs |
| 210 | for part in dir_path.split("/"): |
| 211 | if part in item: |
| 212 | item = item[part] |
| 213 | else: |
| 214 | raise Exception("Directory not found: " + dir_path) |
| 215 | |
| 216 | contents = list( |
| 217 | map( |
| 218 | lambda x: { |
| 219 | "path": join_memfs(dir_path, x[0]), |
| 220 | "entry_name": x[0], |
| 221 | "type": "folder" if isinstance(x[1], dict) else "file", |
| 222 | }, |
| 223 | item.items(), |
| 224 | ) |
| 225 | ) |
| 226 | |
| 227 | return contents |
| 228 | |
| 229 | |
| 230 | def exists_memfs(path): |
| 231 | global memfs |
| 232 | parts = path.split("/") |
| 233 | item = memfs |
| 234 | for part in parts: |
| 235 | if part in item: |
| 236 | item = item[part] |
| 237 | else: |
| 238 | return False |
| 239 | |
| 240 | return True |
| 241 | |
| 242 | |
| 243 | # The below functions force the use of `/` separators in the unit tests |
| 244 | # so that they function on Windows consistently with other platforms. |
| 245 | def join_memfs(path, *paths): |
| 246 | return "/".join([path, *paths]) |
| 247 | |
| 248 | |
| 249 | def resolve_memfs(base, path): |
| 250 | parts = f"{base}/{path}".split("/") |
| 251 | new_parts = [] |
| 252 | for part in parts: |
| 253 | if part == ".": |
| 254 | continue |
| 255 | if part == "..": |
| 256 | new_parts.pop() |
| 257 | continue |
| 258 | new_parts.append(part) |
| 259 | return "/".join(new_parts) |
| 260 | |