FreeCAD

Форк
0
463 строки · 15.4 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# ***************************************************************************
3
# *                                                                         *
4
# *   Copyright (c) 2022-2023 FreeCAD Project Association                   *
5
# *                                                                         *
6
# *   This file is part of FreeCAD.                                         *
7
# *                                                                         *
8
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
9
# *   under the terms of the GNU Lesser General Public License as           *
10
# *   published by the Free Software Foundation, either version 2.1 of the  *
11
# *   License, or (at your option) any later version.                       *
12
# *                                                                         *
13
# *   FreeCAD is distributed in the hope that it will be useful, but        *
14
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
15
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
16
# *   Lesser General Public License for more details.                       *
17
# *                                                                         *
18
# *   You should have received a copy of the GNU Lesser General Public      *
19
# *   License along with FreeCAD. If not, see                               *
20
# *   <https://www.gnu.org/licenses/>.                                      *
21
# *                                                                         *
22
# ***************************************************************************
23

24
"""Mock objects for use when testing the addon manager non-GUI code."""
25

26
# pylint: disable=too-few-public-methods,too-many-instance-attributes,missing-function-docstring
27

28
import os
29
from typing import Union, List
30
import xml.etree.ElementTree as ElemTree
31

32

33
class GitFailed(RuntimeError):
34
    pass
35

36

37
class MockConsole:
38
    """Spy for the FreeCAD.Console -- does NOT print anything out, just logs it."""
39

40
    def __init__(self):
41
        self.log = []
42
        self.messages = []
43
        self.warnings = []
44
        self.errors = []
45

46
    def PrintLog(self, data: str):
47
        self.log.append(data)
48

49
    def PrintMessage(self, data: str):
50
        self.messages.append(data)
51

52
    def PrintWarning(self, data: str):
53
        self.warnings.append(data)
54

55
    def PrintError(self, data: str):
56
        self.errors.append(data)
57

58
    def missing_newlines(self) -> int:
59
        """In most cases, all console entries should end with newlines: this is a
60
        convenience function for unit testing that is true."""
61
        counter = 0
62
        counter += self._count_missing_newlines(self.log)
63
        counter += self._count_missing_newlines(self.messages)
64
        counter += self._count_missing_newlines(self.warnings)
65
        counter += self._count_missing_newlines(self.errors)
66
        return counter
67

68
    @staticmethod
69
    def _count_missing_newlines(some_list) -> int:
70
        counter = 0
71
        for line in some_list:
72
            if line[-1] != "\n":
73
                counter += 1
74
        return counter
75

76

77
class MockAddon:
78
    """Minimal Addon class"""
79

80
    # pylint: disable=too-many-instance-attributes
81

82
    def __init__(
83
        self,
84
        name: str = None,
85
        url: str = None,
86
        status: object = None,
87
        branch: str = "main",
88
    ):
89
        test_dir = os.path.join(os.path.dirname(__file__), "..", "data")
90
        if name:
91
            self.name = name
92
            self.display_name = name
93
        else:
94
            self.name = "MockAddon"
95
            self.display_name = "Mock Addon"
96
        self.url = url if url else os.path.join(test_dir, "test_simple_repo.zip")
97
        self.branch = branch
98
        self.status = status
99
        self.macro = None
100
        self.update_status = None
101
        self.metadata = None
102
        self.icon_file = None
103
        self.last_updated = None
104
        self.requires = set()
105
        self.python_requires = set()
106
        self.python_optional = set()
107
        self.on_git = False
108
        self.on_wiki = True
109

110
    def set_status(self, status):
111
        self.update_status = status
112

113
    @staticmethod
114
    def get_best_icon_relative_path():
115
        return ""
116

117

118
class MockMacro:
119
    """Minimal Macro class"""
120

121
    def __init__(self, name="MockMacro"):
122
        self.name = name
123
        self.filename = self.name + ".FCMacro"
124
        self.icon = ""  # If set, should just be fake filename, doesn't have to exist
125
        self.xpm = ""
126
        self.code = ""
127
        self.raw_code_url = ""
128
        self.other_files = []  # If set, should be fake names, don't have to exist
129
        self.details_filled_from_file = False
130
        self.details_filled_from_code = False
131
        self.parsed_wiki_page = False
132
        self.on_git = False
133
        self.on_wiki = True
134

135
    def install(self, location: os.PathLike):
136
        """Installer function for the mock macro object: creates a file with the src_filename
137
        attribute, and optionally an icon, xpm, and other_files. The data contained in these files
138
        is not usable and serves only as a placeholder for the existence of the files.
139
        """
140

141
        with open(
142
            os.path.join(location, self.filename),
143
            "w",
144
            encoding="utf-8",
145
        ) as f:
146
            f.write("Test file for macro installation unit tests")
147
        if self.icon:
148
            with open(os.path.join(location, self.icon), "wb") as f:
149
                f.write(b"Fake icon data - nothing to see here\n")
150
        if self.xpm:
151
            with open(os.path.join(location, "MockMacro_icon.xpm"), "w", encoding="utf-8") as f:
152
                f.write(self.xpm)
153
        for name in self.other_files:
154
            if "/" in name:
155
                new_location = os.path.dirname(os.path.join(location, name))
156
                os.makedirs(new_location, exist_ok=True)
157
            with open(os.path.join(location, name), "w", encoding="utf-8") as f:
158
                f.write("# Fake macro data for unit testing\n")
159
        return True, []
160

161
    def fill_details_from_file(self, _):
162
        """Tracks that this function was called, but otherwise does nothing"""
163
        self.details_filled_from_file = True
164

165
    def fill_details_from_code(self, _):
166
        self.details_filled_from_code = True
167

168
    def parse_wiki_page(self, _):
169
        self.parsed_wiki_page = True
170

171

172
class SignalCatcher:
173
    """Object to track signals that it has caught.
174

175
    Usage:
176
    catcher = SignalCatcher()
177
    my_signal.connect(catcher.catch_signal)
178
    do_things_that_emit_the_signal()
179
    self.assertTrue(catcher.caught)
180
    """
181

182
    def __init__(self):
183
        self.caught = False
184
        self.killed = False
185
        self.args = None
186

187
    def catch_signal(self, *args):
188
        self.caught = True
189
        self.args = args
190

191
    def die(self):
192
        self.killed = True
193

194

195
class AddonSignalCatcher:
196
    """Signal catcher specifically designed for catching emitted addons."""
197

198
    def __init__(self):
199
        self.addons = []
200

201
    def catch_signal(self, addon):
202
        self.addons.append(addon)
203

204

205
class CallCatcher:
206
    """Generic call monitor -- use to override functions that are not themselves under
207
    test so that you can detect when the function has been called, and how many times.
208
    """
209

210
    def __init__(self):
211
        self.called = False
212
        self.call_count = 0
213
        self.args = None
214

215
    def catch_call(self, *args):
216
        self.called = True
217
        self.call_count += 1
218
        self.args = args
219

220

221
class MockGitManager:
222
    """A mock git manager: does NOT require a git installation. Takes no actions, only records
223
    which functions are called for instrumentation purposes. Can be forced to appear to fail as
224
    needed. Various member variables can be set to emulate necessary return responses.
225
    """
226

227
    def __init__(self):
228
        self.called_methods = []
229
        self.update_available_response = False
230
        self.current_tag_response = "main"
231
        self.current_branch_response = "main"
232
        self.get_remote_response = "No remote set"
233
        self.get_branches_response = ["main"]
234
        self.get_last_committers_response = {"John Doe": {"email": "jdoe@freecad.org", "count": 1}}
235
        self.get_last_authors_response = {"Jane Doe": {"email": "jdoe@freecad.org", "count": 1}}
236
        self.should_fail = False
237
        self.fail_once = False  # Switch back to success after the simulated failure
238

239
    def _check_for_failure(self):
240
        if self.should_fail:
241
            if self.fail_once:
242
                self.should_fail = False
243
            raise GitFailed("Unit test forced failure")
244

245
    def clone(self, _remote, _local_path, _args: List[str] = None):
246
        self.called_methods.append("clone")
247
        self._check_for_failure()
248

249
    def async_clone(self, _remote, _local_path, _progress_monitor, _args: List[str] = None):
250
        self.called_methods.append("async_clone")
251
        self._check_for_failure()
252

253
    def checkout(self, _local_path, _spec, _args: List[str] = None):
254
        self.called_methods.append("checkout")
255
        self._check_for_failure()
256

257
    def update(self, _local_path):
258
        self.called_methods.append("update")
259
        self._check_for_failure()
260

261
    def status(self, _local_path) -> str:
262
        self.called_methods.append("status")
263
        self._check_for_failure()
264
        return "Up-to-date"
265

266
    def reset(self, _local_path, _args: List[str] = None):
267
        self.called_methods.append("reset")
268
        self._check_for_failure()
269

270
    def async_fetch_and_update(self, _local_path, _progress_monitor, _args=None):
271
        self.called_methods.append("async_fetch_and_update")
272
        self._check_for_failure()
273

274
    def update_available(self, _local_path) -> bool:
275
        self.called_methods.append("update_available")
276
        self._check_for_failure()
277
        return self.update_available_response
278

279
    def current_tag(self, _local_path) -> str:
280
        self.called_methods.append("current_tag")
281
        self._check_for_failure()
282
        return self.current_tag_response
283

284
    def current_branch(self, _local_path) -> str:
285
        self.called_methods.append("current_branch")
286
        self._check_for_failure()
287
        return self.current_branch_response
288

289
    def repair(self, _remote, _local_path):
290
        self.called_methods.append("repair")
291
        self._check_for_failure()
292

293
    def get_remote(self, _local_path) -> str:
294
        self.called_methods.append("get_remote")
295
        self._check_for_failure()
296
        return self.get_remote_response
297

298
    def get_branches(self, _local_path) -> List[str]:
299
        self.called_methods.append("get_branches")
300
        self._check_for_failure()
301
        return self.get_branches_response
302

303
    def get_last_committers(self, _local_path, _n=10):
304
        self.called_methods.append("get_last_committers")
305
        self._check_for_failure()
306
        return self.get_last_committers_response
307

308
    def get_last_authors(self, _local_path, _n=10):
309
        self.called_methods.append("get_last_authors")
310
        self._check_for_failure()
311
        return self.get_last_authors_response
312

313

314
class MockSignal:
315
    """A purely synchronous signal, instrumented and intended only for use in unit testing.
316
    emit() is semi-functional, but does not use queued slots so cannot be used across
317
    threads."""
318

319
    def __init__(self, *args):
320
        self.expected_types = args
321
        self.connections = []
322
        self.disconnections = []
323
        self.emitted = False
324

325
    def connect(self, func):
326
        self.connections.append(func)
327

328
    def disconnect(self, func):
329
        if func in self.connections:
330
            self.connections.remove(func)
331
        self.disconnections.append(func)
332

333
    def emit(self, *args):
334
        self.emitted = True
335
        for connection in self.connections:
336
            connection(args)
337

338

339
class MockNetworkManager:
340
    """Instrumented mock for the NetworkManager. Does no network access, is not asynchronous, and
341
    does not require a running event loop. No submitted requests ever complete."""
342

343
    def __init__(self):
344
        self.urls = []
345
        self.aborted = []
346
        self.data = MockByteArray()
347
        self.called_methods = []
348

349
        self.completed = MockSignal(int, int, MockByteArray)
350
        self.progress_made = MockSignal(int, int, int)
351
        self.progress_complete = MockSignal(int, int, os.PathLike)
352

353
    def submit_unmonitored_get(self, url: str) -> int:
354
        self.urls.append(url)
355
        self.called_methods.append("submit_unmonitored_get")
356
        return len(self.urls) - 1
357

358
    def submit_monitored_get(self, url: str) -> int:
359
        self.urls.append(url)
360
        self.called_methods.append("submit_monitored_get")
361
        return len(self.urls) - 1
362

363
    def blocking_get(self, url: str):
364
        self.urls.append(url)
365
        self.called_methods.append("blocking_get")
366
        return self.data
367

368
    def abort_all(self):
369
        self.called_methods.append("abort_all")
370
        for url in self.urls:
371
            self.aborted.append(url)
372

373
    def abort(self, index: int):
374
        self.called_methods.append("abort")
375
        self.aborted.append(self.urls[index])
376

377

378
class MockByteArray:
379
    """Mock for QByteArray. Only provides the data() access member."""
380

381
    def __init__(self, data_to_wrap="data".encode("utf-8")):
382
        self.wrapped = data_to_wrap
383

384
    def data(self) -> bytes:
385
        return self.wrapped
386

387

388
class MockThread:
389
    """Mock for QThread for use when threading is not being used, but interruption
390
    needs to be tested. Set interrupt_after_n_calls to the call number to stop at."""
391

392
    def __init__(self):
393
        self.interrupt_after_n_calls = 0
394
        self.interrupt_check_counter = 0
395

396
    def isInterruptionRequested(self):
397
        self.interrupt_check_counter += 1
398
        if (
399
            self.interrupt_after_n_calls
400
            and self.interrupt_check_counter >= self.interrupt_after_n_calls
401
        ):
402
            return True
403
        return False
404

405

406
class MockPref:
407
    def __init__(self):
408
        self.prefs = {}
409
        self.pref_set_counter = {}
410
        self.pref_get_counter = {}
411

412
    def set_prefs(self, pref_dict: dict) -> None:
413
        self.prefs = pref_dict
414

415
    def GetInt(self, key: str, default: int) -> int:
416
        return self.Get(key, default)
417

418
    def GetString(self, key: str, default: str) -> str:
419
        return self.Get(key, default)
420

421
    def GetBool(self, key: str, default: bool) -> bool:
422
        return self.Get(key, default)
423

424
    def Get(self, key: str, default):
425
        if key not in self.pref_set_counter:
426
            self.pref_get_counter[key] = 1
427
        else:
428
            self.pref_get_counter[key] += 1
429
        if key in self.prefs:
430
            return self.prefs[key]
431
        raise ValueError(f"Expected key not in mock preferences: {key}")
432

433
    def SetInt(self, key: str, value: int) -> None:
434
        return self.Set(key, value)
435

436
    def SetString(self, key: str, value: str) -> None:
437
        return self.Set(key, value)
438

439
    def SetBool(self, key: str, value: bool) -> None:
440
        return self.Set(key, value)
441

442
    def Set(self, key: str, value):
443
        if key not in self.pref_set_counter:
444
            self.pref_set_counter[key] = 1
445
        else:
446
            self.pref_set_counter[key] += 1
447
        self.prefs[key] = value
448

449

450
class MockExists:
451
    def __init__(self, files: List[str] = None):
452
        """Returns True for all files in files, and False for all others"""
453
        self.files = files
454
        self.files_checked = []
455

456
    def exists(self, check_file: str):
457
        self.files_checked.append(check_file)
458
        if not self.files:
459
            return False
460
        for file in self.files:
461
            if check_file.endswith(file):
462
                return True
463
        return False
464

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.