Addon Manager: Move to git submodule (#20672)
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
||||
[submodule "src/3rdParty/GSL"]
|
||||
path = src/3rdParty/GSL
|
||||
url = https://github.com/microsoft/GSL
|
||||
[submodule "src/Mod/AddonManager"]
|
||||
path = src/Mod/AddonManager
|
||||
url = https://github.com/FreeCAD/AddonManager.git
|
||||
|
||||
@@ -9,7 +9,6 @@ files: |
|
||||
src/Main|
|
||||
src/Tools|
|
||||
tests/src|
|
||||
src/Mod/AddonManager|
|
||||
src/Mod/Assembly|
|
||||
src/Mod/CAM|
|
||||
src/Mod/Cloud|
|
||||
|
||||
1
src/Mod/AddonManager
Submodule
@@ -1,35 +0,0 @@
|
||||
# This file lists the Python packages that the Addon Manager allows to be installed
|
||||
# automatically via pip. To request that a package be added to this list, submit a
|
||||
# pull request to the FreeCAD git repository with the requested package added. Only
|
||||
# packages in this list will be processed from the metadata.txt and requirements.txt
|
||||
# files specified by an Addon. Note that this is NOT a requirements.txt-format file,
|
||||
# no version information may be specified, and no wildcards are supported.
|
||||
|
||||
# Allow these packages to be installed:
|
||||
aiofiles
|
||||
autobahn
|
||||
ezdxf
|
||||
gmsh
|
||||
gmsh-dev
|
||||
lxml
|
||||
markdown
|
||||
matplotlib
|
||||
msgpack
|
||||
numpy
|
||||
ocp
|
||||
olefile
|
||||
openpyxl
|
||||
pandas
|
||||
pillow
|
||||
ply
|
||||
py-slvs
|
||||
pycollada
|
||||
pygit2
|
||||
pynastran
|
||||
requests
|
||||
rhino3dm
|
||||
scipy
|
||||
xlrd
|
||||
xlutils
|
||||
xlwt
|
||||
PyYAML
|
||||
@@ -1,863 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Defines the Addon class to encapsulate information about FreeCAD Addons"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
from typing import Dict, Set, List, Optional
|
||||
from threading import Lock
|
||||
from enum import IntEnum, auto
|
||||
import xml.etree.ElementTree
|
||||
|
||||
import addonmanager_freecad_interface as fci
|
||||
from addonmanager_macro import Macro
|
||||
import addonmanager_utilities as utils
|
||||
from addonmanager_utilities import construct_git_url, process_date_string_to_python_datetime
|
||||
from addonmanager_metadata import (
|
||||
Metadata,
|
||||
MetadataReader,
|
||||
UrlType,
|
||||
Version,
|
||||
DependencyType,
|
||||
)
|
||||
from AddonStats import AddonStats
|
||||
|
||||
translate = fci.translate
|
||||
|
||||
# A list of internal workbenches that can be used as a dependency of an Addon
|
||||
INTERNAL_WORKBENCHES = {
|
||||
"bim": "BIM",
|
||||
"assembly": "Assembly",
|
||||
"draft": "Draft",
|
||||
"fem": "FEM",
|
||||
"mesh": "Mesh",
|
||||
"openscad": "OpenSCAD",
|
||||
"part": "Part",
|
||||
"partdesign": "PartDesign",
|
||||
"cam": "CAM",
|
||||
"plot": "Plot",
|
||||
"points": "Points",
|
||||
"robot": "Robot",
|
||||
"sketcher": "Sketcher",
|
||||
"spreadsheet": "Spreadsheet",
|
||||
"techdraw": "TechDraw",
|
||||
}
|
||||
|
||||
|
||||
class Addon:
|
||||
"""Encapsulates information about a FreeCAD addon"""
|
||||
|
||||
class Kind(IntEnum):
|
||||
"""The type of Addon: Workbench, macro, or package"""
|
||||
|
||||
WORKBENCH = 1
|
||||
MACRO = 2
|
||||
PACKAGE = 3
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.value == 1:
|
||||
return "Workbench"
|
||||
if self.value == 2:
|
||||
return "Macro"
|
||||
if self.value == 3:
|
||||
return "Package"
|
||||
return "ERROR_TYPE"
|
||||
|
||||
class Status(IntEnum):
|
||||
"""The installation status of an Addon"""
|
||||
|
||||
NOT_INSTALLED = 0
|
||||
UNCHECKED = 1
|
||||
NO_UPDATE_AVAILABLE = 2
|
||||
UPDATE_AVAILABLE = 3
|
||||
PENDING_RESTART = 4
|
||||
CANNOT_CHECK = 5 # If we don't have git, etc.
|
||||
UNKNOWN = 100
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.__class__ is other.__class__:
|
||||
return self.value < other.value
|
||||
return NotImplemented
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.value == 0:
|
||||
result = "Not installed"
|
||||
elif self.value == 1:
|
||||
result = "Unchecked"
|
||||
elif self.value == 2:
|
||||
result = "No update available"
|
||||
elif self.value == 3:
|
||||
result = "Update available"
|
||||
elif self.value == 4:
|
||||
result = "Restart required"
|
||||
elif self.value == 5:
|
||||
result = "Can't check"
|
||||
else:
|
||||
result = "ERROR_STATUS"
|
||||
return result
|
||||
|
||||
class Dependencies:
|
||||
"""Addon dependency information"""
|
||||
|
||||
def __init__(self):
|
||||
self.required_external_addons = [] # A list of Addons
|
||||
self.blockers = [] # A list of Addons
|
||||
self.replaces = [] # A list of Addons
|
||||
self.internal_workbenches: Set[str] = set() # Required internal workbenches
|
||||
self.python_requires: Set[str] = set()
|
||||
self.python_optional: Set[str] = set()
|
||||
self.python_min_version = {"major": 3, "minor": 0}
|
||||
|
||||
class DependencyType(IntEnum):
|
||||
"""Several types of dependency information is stored"""
|
||||
|
||||
INTERNAL_WORKBENCH = auto()
|
||||
REQUIRED_ADDON = auto()
|
||||
BLOCKED_ADDON = auto()
|
||||
REPLACED_ADDON = auto()
|
||||
REQUIRED_PYTHON = auto()
|
||||
OPTIONAL_PYTHON = auto()
|
||||
|
||||
class ResolutionFailed(RuntimeError):
|
||||
"""An exception type for dependency resolution failure."""
|
||||
|
||||
# The location of Addon Manager cache files: overridden by testing code
|
||||
cache_directory = os.path.join(fci.DataPaths().cache_dir, "AddonManager")
|
||||
|
||||
# The location of the Mod directory: overridden by testing code
|
||||
mod_directory = fci.DataPaths().mod_dir
|
||||
|
||||
# The location of the Macro directory: overridden by testing code
|
||||
macro_directory = fci.DataPaths().macro_dir
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
url: str = "",
|
||||
status: Status = Status.UNKNOWN,
|
||||
branch: str = "",
|
||||
):
|
||||
self.name = name.strip()
|
||||
self.display_name = self.name
|
||||
self.url = url.strip()
|
||||
self.branch = branch.strip()
|
||||
self.python2 = False
|
||||
self.obsolete = False
|
||||
self.rejected = False
|
||||
self.repo_type = Addon.Kind.WORKBENCH
|
||||
self.description = None
|
||||
self.tags = set() # Just a cache, loaded from Metadata
|
||||
self.last_updated = None
|
||||
self.stats = AddonStats()
|
||||
self.score = 0
|
||||
|
||||
# To prevent multiple threads from running git actions on this repo at the
|
||||
# same time
|
||||
self.git_lock = Lock()
|
||||
|
||||
# To prevent multiple threads from accessing the status at the same time
|
||||
self.status_lock = Lock()
|
||||
self.update_status = status
|
||||
|
||||
self._clean_url()
|
||||
|
||||
if utils.recognized_git_location(self):
|
||||
self.metadata_url = construct_git_url(self, "package.xml")
|
||||
else:
|
||||
self.metadata_url = None
|
||||
self.metadata: Optional[Metadata] = None
|
||||
self.icon = None # A QIcon version of this Addon's icon
|
||||
self.icon_file: str = "" # Absolute local path to cached icon file
|
||||
self.best_icon_relative_path = ""
|
||||
self.macro = None # Bridge to Gaël Écorchard's macro management class
|
||||
self.updated_timestamp = None
|
||||
self.installed_version = None
|
||||
self.installed_metadata = None
|
||||
|
||||
# Each repo is also a node in a directed dependency graph (referenced by name so
|
||||
# they can be serialized):
|
||||
self.requires: Set[str] = set()
|
||||
self.blocks: Set[str] = set()
|
||||
|
||||
# And maintains a list of required and optional Python dependencies
|
||||
self.python_requires: Set[str] = set()
|
||||
self.python_optional: Set[str] = set()
|
||||
self.python_min_version = {"major": 3, "minor": 0}
|
||||
|
||||
self._icon_file = None
|
||||
self._cached_license: str = ""
|
||||
self._cached_update_date = None
|
||||
|
||||
def _clean_url(self):
|
||||
# The url should never end in ".git", so strip it if it's there
|
||||
parsed_url = urlparse(self.url)
|
||||
if parsed_url.path.endswith(".git"):
|
||||
self.url = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path[:-4]
|
||||
if parsed_url.query:
|
||||
self.url += "?" + parsed_url.query
|
||||
if parsed_url.fragment:
|
||||
self.url += "#" + parsed_url.fragment
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = f"FreeCAD {self.repo_type}\n"
|
||||
result += f"Name: {self.name}\n"
|
||||
result += f"URL: {self.url}\n"
|
||||
result += "Has metadata\n" if self.metadata is not None else "No metadata found\n"
|
||||
if self.macro is not None:
|
||||
result += "Has linked Macro object\n"
|
||||
return result
|
||||
|
||||
@property
|
||||
def license(self):
|
||||
if not self._cached_license:
|
||||
self._cached_license = "UNLICENSED"
|
||||
if self.metadata and self.metadata.license:
|
||||
self._cached_license = self.metadata.license
|
||||
elif self.stats and self.stats.license:
|
||||
self._cached_license = self.stats.license
|
||||
elif self.macro:
|
||||
if self.macro.license:
|
||||
self._cached_license = self.macro.license
|
||||
elif self.macro.on_wiki:
|
||||
self._cached_license = "CC-BY-3.0"
|
||||
return self._cached_license
|
||||
|
||||
@property
|
||||
def update_date(self):
|
||||
if self._cached_update_date is None:
|
||||
self._cached_update_date = 0
|
||||
if self.stats and self.stats.last_update_time:
|
||||
self._cached_update_date = self.stats.last_update_time
|
||||
elif self.macro and self.macro.date:
|
||||
# Try to parse the date:
|
||||
try:
|
||||
self._cached_update_date = process_date_string_to_python_datetime(
|
||||
self.macro.date
|
||||
)
|
||||
except ValueError as e:
|
||||
fci.Console.PrintWarning(str(e) + "\n")
|
||||
else:
|
||||
fci.Console.PrintWarning(f"No update date info for {self.name}\n")
|
||||
return self._cached_update_date
|
||||
|
||||
@classmethod
|
||||
def from_macro(cls, macro: Macro):
|
||||
"""Create an Addon object from a Macro wrapper object"""
|
||||
|
||||
if macro.is_installed():
|
||||
status = Addon.Status.UNCHECKED
|
||||
else:
|
||||
status = Addon.Status.NOT_INSTALLED
|
||||
instance = Addon(macro.name, macro.url, status, "master")
|
||||
instance.macro = macro
|
||||
instance.repo_type = Addon.Kind.MACRO
|
||||
instance.description = macro.desc
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_cache(cls, cache_dict: Dict):
|
||||
"""Load basic data from cached dict data. Does not include Macro or Metadata
|
||||
information, which must be populated separately."""
|
||||
|
||||
mod_dir = os.path.join(cls.mod_directory, cache_dict["name"])
|
||||
if os.path.isdir(mod_dir):
|
||||
status = Addon.Status.UNCHECKED
|
||||
else:
|
||||
status = Addon.Status.NOT_INSTALLED
|
||||
instance = Addon(cache_dict["name"], cache_dict["url"], status, cache_dict["branch"])
|
||||
|
||||
for key, value in cache_dict.items():
|
||||
if not str(key).startswith("_"):
|
||||
instance.__dict__[key] = value
|
||||
|
||||
instance.repo_type = Addon.Kind(cache_dict["repo_type"])
|
||||
if instance.repo_type == Addon.Kind.PACKAGE:
|
||||
# There must be a cached metadata file, too
|
||||
cached_package_xml_file = os.path.join(
|
||||
instance.cache_directory,
|
||||
"PackageMetadata",
|
||||
instance.name,
|
||||
)
|
||||
if os.path.isfile(cached_package_xml_file):
|
||||
instance.load_metadata_file(cached_package_xml_file)
|
||||
|
||||
instance._load_installed_metadata()
|
||||
|
||||
if "requires" in cache_dict:
|
||||
instance.requires = set(cache_dict["requires"])
|
||||
instance.blocks = set(cache_dict["blocks"])
|
||||
instance.python_requires = set(cache_dict["python_requires"])
|
||||
instance.python_optional = set(cache_dict["python_optional"])
|
||||
|
||||
instance._clean_url()
|
||||
|
||||
return instance
|
||||
|
||||
def to_cache(self) -> Dict:
|
||||
"""Returns a dictionary with cache information that can be used later with
|
||||
from_cache to recreate this object."""
|
||||
|
||||
return {
|
||||
"name": self.name,
|
||||
"display_name": self.display_name,
|
||||
"url": self.url,
|
||||
"branch": self.branch,
|
||||
"repo_type": int(self.repo_type),
|
||||
"description": self.description,
|
||||
"cached_icon_filename": self.get_cached_icon_filename(),
|
||||
"best_icon_relative_path": self.get_best_icon_relative_path(),
|
||||
"python2": self.python2,
|
||||
"obsolete": self.obsolete,
|
||||
"rejected": self.rejected,
|
||||
"requires": list(self.requires),
|
||||
"blocks": list(self.blocks),
|
||||
"python_requires": list(self.python_requires),
|
||||
"python_optional": list(self.python_optional),
|
||||
}
|
||||
|
||||
def load_metadata_file(self, file: str) -> None:
|
||||
"""Read a given metadata file and set it as this object's metadata"""
|
||||
|
||||
if os.path.exists(file):
|
||||
try:
|
||||
metadata = MetadataReader.from_file(file)
|
||||
except xml.etree.ElementTree.ParseError:
|
||||
fci.Console.PrintWarning(
|
||||
"An invalid or corrupted package.xml file was found in the cache for"
|
||||
)
|
||||
fci.Console.PrintWarning(f" {self.name}... ignoring the bad data.\n")
|
||||
return
|
||||
self.set_metadata(metadata)
|
||||
self._clean_url()
|
||||
else:
|
||||
fci.Console.PrintLog(f"Internal error: {file} does not exist")
|
||||
|
||||
def _load_installed_metadata(self) -> None:
|
||||
# If it is actually installed, there is a SECOND metadata file, in the actual installation,
|
||||
# that may not match the cached one if the Addon has not been updated but the cache has.
|
||||
mod_dir = os.path.join(self.mod_directory, self.name)
|
||||
installed_metadata_path = os.path.join(mod_dir, "package.xml")
|
||||
if os.path.isfile(installed_metadata_path):
|
||||
try:
|
||||
self.installed_metadata = MetadataReader.from_file(installed_metadata_path)
|
||||
except xml.etree.ElementTree.ParseError:
|
||||
fci.Console.PrintWarning(
|
||||
"An invalid or corrupted package.xml file was found in installation of"
|
||||
)
|
||||
fci.Console.PrintWarning(f" {self.name}... ignoring the bad data.\n")
|
||||
return
|
||||
|
||||
def set_metadata(self, metadata: Metadata) -> None:
|
||||
"""Set the given metadata object as this object's metadata, updating the
|
||||
object's display name and package type information to match, as well as
|
||||
updating any dependency information, etc.
|
||||
"""
|
||||
|
||||
self.metadata = metadata
|
||||
self.display_name = metadata.name
|
||||
self.repo_type = Addon.Kind.PACKAGE
|
||||
self.description = metadata.description
|
||||
for url in metadata.url:
|
||||
if url.type == UrlType.repository:
|
||||
self.url = url.location
|
||||
self.branch = url.branch if url.branch else "master"
|
||||
self._clean_url()
|
||||
self.extract_tags(self.metadata)
|
||||
self.extract_metadata_dependencies(self.metadata)
|
||||
|
||||
@staticmethod
|
||||
def version_is_ok(metadata: Metadata) -> bool:
|
||||
"""Checks to see if the current running version of FreeCAD meets the
|
||||
requirements set by the passed-in metadata parameter."""
|
||||
|
||||
from_fci = list(fci.Version())
|
||||
fc_version = Version(from_list=from_fci)
|
||||
|
||||
dep_fc_min = metadata.freecadmin if metadata.freecadmin else fc_version
|
||||
dep_fc_max = metadata.freecadmax if metadata.freecadmax else fc_version
|
||||
|
||||
return dep_fc_min <= fc_version <= dep_fc_max
|
||||
|
||||
def extract_metadata_dependencies(self, metadata: Metadata):
|
||||
"""Read dependency information from a metadata object and store it in this
|
||||
Addon"""
|
||||
|
||||
# Version check: if this piece of metadata doesn't apply to this version of
|
||||
# FreeCAD, just skip it.
|
||||
if not Addon.version_is_ok(metadata):
|
||||
return
|
||||
|
||||
if metadata.pythonmin:
|
||||
self.python_min_version["major"] = metadata.pythonmin.version_as_list[0]
|
||||
self.python_min_version["minor"] = metadata.pythonmin.version_as_list[1]
|
||||
|
||||
for dep in metadata.depend:
|
||||
if dep.dependency_type == DependencyType.internal:
|
||||
if dep.package in INTERNAL_WORKBENCHES:
|
||||
self.requires.add(dep.package)
|
||||
else:
|
||||
fci.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"{}: Unrecognized internal workbench '{}'",
|
||||
).format(self.name, dep.package)
|
||||
)
|
||||
elif dep.dependency_type == DependencyType.addon:
|
||||
self.requires.add(dep.package)
|
||||
elif dep.dependency_type == DependencyType.python:
|
||||
if dep.optional:
|
||||
self.python_optional.add(dep.package)
|
||||
else:
|
||||
self.python_requires.add(dep.package)
|
||||
else:
|
||||
# Automatic resolution happens later, once we have a complete list of
|
||||
# Addons
|
||||
self.requires.add(dep.package)
|
||||
|
||||
for dep in metadata.conflict:
|
||||
self.blocks.add(dep.package)
|
||||
|
||||
# Recurse
|
||||
content = metadata.content
|
||||
for _, value in content.items():
|
||||
for item in value:
|
||||
self.extract_metadata_dependencies(item)
|
||||
|
||||
def verify_url_and_branch(self, url: str, branch: str) -> None:
|
||||
"""Print diagnostic information for Addon Developers if their metadata is
|
||||
inconsistent with the actual fetch location. Most often this is due to using
|
||||
the wrong branch name."""
|
||||
|
||||
if self.url != url:
|
||||
fci.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Addon Developer Warning: Repository URL set in package.xml file for addon {} ({}) does not match the URL it was fetched from ({})",
|
||||
).format(self.display_name, self.url, url)
|
||||
+ "\n"
|
||||
)
|
||||
if self.branch != branch:
|
||||
fci.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Addon Developer Warning: Repository branch set in package.xml file for addon {} ({}) does not match the branch it was fetched from ({})",
|
||||
).format(self.display_name, self.branch, branch)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
def extract_tags(self, metadata: Metadata) -> None:
|
||||
"""Read the tags from the metadata object"""
|
||||
|
||||
# Version check: if this piece of metadata doesn't apply to this version of
|
||||
# FreeCAD, just skip it.
|
||||
if not Addon.version_is_ok(metadata):
|
||||
return
|
||||
|
||||
for new_tag in metadata.tag:
|
||||
self.tags.add(new_tag)
|
||||
|
||||
content = metadata.content
|
||||
for _, value in content.items():
|
||||
for item in value:
|
||||
self.extract_tags(item)
|
||||
|
||||
def contains_workbench(self) -> bool:
|
||||
"""Determine if this package contains (or is) a workbench"""
|
||||
|
||||
if self.repo_type == Addon.Kind.WORKBENCH:
|
||||
return True
|
||||
return self.contains_packaged_content("workbench")
|
||||
|
||||
def contains_macro(self) -> bool:
|
||||
"""Determine if this package contains (or is) a macro"""
|
||||
|
||||
if self.repo_type == Addon.Kind.MACRO:
|
||||
return True
|
||||
return self.contains_packaged_content("macro")
|
||||
|
||||
def contains_packaged_content(self, content_type: str):
|
||||
"""Determine if the package contains content_type"""
|
||||
if self.repo_type == Addon.Kind.PACKAGE:
|
||||
if self.metadata is None:
|
||||
fci.Console.PrintLog(
|
||||
f"Addon Manager internal error: lost metadata for package {self.name}\n"
|
||||
)
|
||||
return False
|
||||
content = self.metadata.content
|
||||
return content_type in content
|
||||
return False
|
||||
|
||||
def contains_preference_pack(self) -> bool:
|
||||
"""Determine if this package contains a preference pack"""
|
||||
return self.contains_packaged_content("preferencepack")
|
||||
|
||||
def contains_bundle(self) -> bool:
|
||||
"""Determine if this package contains a bundle"""
|
||||
return self.contains_packaged_content("bundle")
|
||||
|
||||
def contains_other(self) -> bool:
|
||||
"""Determine if this package contains an "other" content item"""
|
||||
return self.contains_packaged_content("other")
|
||||
|
||||
def get_best_icon_relative_path(self) -> str:
|
||||
"""Get the path within the repo the addon's icon. Usually specified by
|
||||
top-level metadata, but some authors omit it and specify only icons for the
|
||||
contents. Find the first one of those, in such cases."""
|
||||
|
||||
if self.best_icon_relative_path:
|
||||
return self.best_icon_relative_path
|
||||
|
||||
if not self.metadata:
|
||||
return ""
|
||||
|
||||
real_icon = self.metadata.icon
|
||||
if not real_icon:
|
||||
# If there is no icon set for the entire package, see if there are any
|
||||
# workbenches, which are required to have icons, and grab the first one
|
||||
# we find:
|
||||
content = self.metadata.content
|
||||
if "workbench" in content:
|
||||
wb = content["workbench"][0]
|
||||
if wb.icon:
|
||||
if wb.subdirectory:
|
||||
subdir = wb.subdirectory
|
||||
else:
|
||||
subdir = wb.name
|
||||
real_icon = subdir + wb.icon
|
||||
|
||||
self.best_icon_relative_path = real_icon
|
||||
return self.best_icon_relative_path
|
||||
|
||||
def get_cached_icon_filename(self) -> str:
|
||||
"""NOTE: This function is deprecated and will be removed in a coming update."""
|
||||
|
||||
if hasattr(self, "cached_icon_filename") and self.cached_icon_filename:
|
||||
return self.cached_icon_filename
|
||||
|
||||
if not self.metadata:
|
||||
return ""
|
||||
|
||||
real_icon = self.metadata.icon
|
||||
if not real_icon:
|
||||
# If there is no icon set for the entire package, see if there are any
|
||||
# workbenches, which are required to have icons, and grab the first one
|
||||
# we find:
|
||||
content = self.metadata.content
|
||||
if "workbench" in content:
|
||||
wb = content["workbench"][0]
|
||||
if wb.icon:
|
||||
if wb.subdirectory:
|
||||
subdir = wb.subdirectory
|
||||
else:
|
||||
subdir = wb.name
|
||||
real_icon = subdir + wb.icon
|
||||
|
||||
real_icon = real_icon.replace(
|
||||
"/", os.path.sep
|
||||
) # Required path separator in the metadata.xml file to local separator
|
||||
|
||||
_, file_extension = os.path.splitext(real_icon)
|
||||
store = os.path.join(self.cache_directory, "PackageMetadata")
|
||||
self.cached_icon_filename = os.path.join(store, self.name, "cached_icon" + file_extension)
|
||||
|
||||
return self.cached_icon_filename
|
||||
|
||||
def walk_dependency_tree(self, all_repos, deps):
|
||||
"""Compute the total dependency tree for this repo (recursive)
|
||||
- all_repos is a dictionary of repos, keyed on the name of the repo
|
||||
- deps is an Addon.Dependency object encapsulating all the types of dependency
|
||||
information that may be needed.
|
||||
"""
|
||||
|
||||
deps.python_requires |= self.python_requires
|
||||
deps.python_optional |= self.python_optional
|
||||
|
||||
deps.python_min_version["major"] = max(
|
||||
deps.python_min_version["major"], self.python_min_version["major"]
|
||||
)
|
||||
if deps.python_min_version["major"] == 3:
|
||||
deps.python_min_version["minor"] = max(
|
||||
deps.python_min_version["minor"], self.python_min_version["minor"]
|
||||
)
|
||||
else:
|
||||
fci.Console.PrintWarning("Unrecognized Python version information")
|
||||
|
||||
for dep in self.requires:
|
||||
if dep in all_repos:
|
||||
if dep not in deps.required_external_addons:
|
||||
deps.required_external_addons.append(all_repos[dep])
|
||||
all_repos[dep].walk_dependency_tree(all_repos, deps)
|
||||
else:
|
||||
# See if this is an internal workbench:
|
||||
if dep.upper().endswith("WB"):
|
||||
real_name = dep[:-2].strip().lower()
|
||||
elif dep.upper().endswith("WORKBENCH"):
|
||||
real_name = dep[:-9].strip().lower()
|
||||
else:
|
||||
real_name = dep.strip().lower()
|
||||
|
||||
if real_name in INTERNAL_WORKBENCHES:
|
||||
deps.internal_workbenches.add(INTERNAL_WORKBENCHES[real_name])
|
||||
else:
|
||||
# Assume it's a Python requirement of some kind:
|
||||
deps.python_requires.add(dep)
|
||||
|
||||
for dep in self.blocks:
|
||||
if dep in all_repos:
|
||||
deps.blockers[dep] = all_repos[dep]
|
||||
|
||||
def status(self):
|
||||
"""Threadsafe access to the current update status"""
|
||||
with self.status_lock:
|
||||
return self.update_status
|
||||
|
||||
def set_status(self, status):
|
||||
"""Threadsafe setting of the update status"""
|
||||
with self.status_lock:
|
||||
self.update_status = status
|
||||
|
||||
def is_disabled(self):
|
||||
"""Check to see if the disabling stopfile exists"""
|
||||
|
||||
stopfile = os.path.join(self.mod_directory, self.name, "ADDON_DISABLED")
|
||||
return os.path.exists(stopfile)
|
||||
|
||||
def disable(self):
|
||||
"""Disable this addon from loading when FreeCAD starts up by creating a
|
||||
stopfile"""
|
||||
|
||||
stopfile = os.path.join(self.mod_directory, self.name, "ADDON_DISABLED")
|
||||
with open(stopfile, "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"The existence of this file prevents FreeCAD from loading this Addon. To re-enable, delete the file."
|
||||
)
|
||||
|
||||
if self.contains_workbench():
|
||||
self.disable_workbench()
|
||||
|
||||
def enable(self):
|
||||
"""Re-enable loading this addon by deleting the stopfile"""
|
||||
|
||||
stopfile = os.path.join(self.mod_directory, self.name, "ADDON_DISABLED")
|
||||
try:
|
||||
os.unlink(stopfile)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if self.contains_workbench():
|
||||
self.enable_workbench()
|
||||
|
||||
def enable_workbench(self):
|
||||
wbName = self.get_workbench_name()
|
||||
|
||||
# Remove from the list of disabled.
|
||||
self.remove_from_disabled_wbs(wbName)
|
||||
|
||||
def disable_workbench(self):
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches")
|
||||
wbName = self.get_workbench_name()
|
||||
|
||||
# Add the wb to the list of disabled if it was not already
|
||||
disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench")
|
||||
# print(f"start disabling {disabled_wbs}")
|
||||
disabled_wbs_list = disabled_wbs.split(",")
|
||||
if not (wbName in disabled_wbs_list):
|
||||
disabled_wbs += "," + wbName
|
||||
pref.SetString("Disabled", disabled_wbs)
|
||||
# print(f"done disabling : {disabled_wbs} \n")
|
||||
|
||||
def desinstall_workbench(self):
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches")
|
||||
wbName = self.get_workbench_name()
|
||||
|
||||
# Remove from the list of ordered.
|
||||
ordered_wbs = pref.GetString("Ordered", "")
|
||||
# print(f"start remove from ordering {ordered_wbs}")
|
||||
ordered_wbs_list = ordered_wbs.split(",")
|
||||
ordered_wbs = ""
|
||||
for wb in ordered_wbs_list:
|
||||
if wb != wbName:
|
||||
if ordered_wbs != "":
|
||||
ordered_wbs += ","
|
||||
ordered_wbs += wb
|
||||
pref.SetString("Ordered", ordered_wbs)
|
||||
# print(f"end remove from ordering {ordered_wbs}")
|
||||
|
||||
# Remove from the list of disabled.
|
||||
self.remove_from_disabled_wbs(wbName)
|
||||
|
||||
def remove_from_disabled_wbs(self, wbName: str):
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches")
|
||||
|
||||
disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench")
|
||||
# print(f"start enabling : {disabled_wbs}")
|
||||
disabled_wbs_list = disabled_wbs.split(",")
|
||||
disabled_wbs = ""
|
||||
for wb in disabled_wbs_list:
|
||||
if wb != wbName:
|
||||
if disabled_wbs != "":
|
||||
disabled_wbs += ","
|
||||
disabled_wbs += wb
|
||||
pref.SetString("Disabled", disabled_wbs)
|
||||
# print(f"Done enabling {disabled_wbs} \n")
|
||||
|
||||
def get_workbench_name(self) -> str:
|
||||
"""Find the name of the workbench class (ie the name under which it's
|
||||
registered in freecad core)'"""
|
||||
wb_name = ""
|
||||
|
||||
if self.repo_type == Addon.Kind.PACKAGE:
|
||||
for wb in self.metadata.content["workbench"]: # we may have more than one wb.
|
||||
if wb_name != "":
|
||||
wb_name += ","
|
||||
wb_name += wb.classname
|
||||
if self.repo_type == Addon.Kind.WORKBENCH or wb_name == "":
|
||||
wb_name = self.try_find_wbname_in_files()
|
||||
if wb_name == "":
|
||||
wb_name = self.name
|
||||
return wb_name
|
||||
|
||||
def try_find_wbname_in_files(self) -> str:
|
||||
"""Attempt to locate a line with an addWorkbench command in the workbench's
|
||||
Python files. If it is directly instantiating a workbench, then we can use
|
||||
the line to determine classname for this workbench. If it uses a variable,
|
||||
or if the line doesn't exist at all, an empty string is returned."""
|
||||
mod_dir = os.path.join(self.mod_directory, self.name)
|
||||
|
||||
for root, _, files in os.walk(mod_dir):
|
||||
for f in files:
|
||||
current_file = os.path.join(root, f)
|
||||
if not os.path.isdir(current_file):
|
||||
filename, extension = os.path.splitext(current_file)
|
||||
if extension == ".py":
|
||||
wb_classname = self._find_classname_in_file(current_file)
|
||||
if wb_classname:
|
||||
return wb_classname
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _find_classname_in_file(current_file) -> str:
|
||||
try:
|
||||
with open(current_file, "r", encoding="utf-8") as python_file:
|
||||
content = python_file.read()
|
||||
search_result = re.search(r"Gui.addWorkbench\s*\(\s*(\w+)\s*\(\s*\)\s*\)", content)
|
||||
if search_result:
|
||||
return search_result.group(1)
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
# @dataclass(frozen)
|
||||
class MissingDependencies:
|
||||
"""Encapsulates a group of four types of dependencies:
|
||||
* Internal workbenches -> wbs
|
||||
* External addons -> external_addons
|
||||
* Required Python packages -> python_requires
|
||||
* Optional Python packages -> python_optional
|
||||
"""
|
||||
|
||||
def __init__(self, repo: Addon, all_repos: List[Addon]):
|
||||
deps = Addon.Dependencies()
|
||||
repo_name_dict = {}
|
||||
for r in all_repos:
|
||||
repo_name_dict[r.name] = r
|
||||
if hasattr(r, "display_name"):
|
||||
# Test harness might not provide a display name
|
||||
repo_name_dict[r.display_name] = r
|
||||
|
||||
if hasattr(repo, "walk_dependency_tree"):
|
||||
# Sometimes the test harness doesn't provide this function, to override
|
||||
# any dependency checking
|
||||
repo.walk_dependency_tree(repo_name_dict, deps)
|
||||
|
||||
self.external_addons = []
|
||||
for dep in deps.required_external_addons:
|
||||
if dep.status() == Addon.Status.NOT_INSTALLED:
|
||||
self.external_addons.append(dep.name)
|
||||
|
||||
# Now check the loaded addons to see if we are missing an internal workbench:
|
||||
if fci.FreeCADGui:
|
||||
wbs = [wb.lower() for wb in fci.FreeCADGui.listWorkbenches()]
|
||||
else:
|
||||
wbs = []
|
||||
|
||||
self.wbs = []
|
||||
for dep in deps.internal_workbenches:
|
||||
if dep.lower() + "workbench" not in wbs:
|
||||
if dep.lower() == "plot":
|
||||
# Special case for plot, which is no longer a full workbench:
|
||||
try:
|
||||
__import__("Plot")
|
||||
except ImportError:
|
||||
# Plot might fail for a number of reasons
|
||||
self.wbs.append(dep)
|
||||
fci.Console.PrintLog("Failed to import Plot module")
|
||||
else:
|
||||
self.wbs.append(dep)
|
||||
|
||||
# Check the Python dependencies:
|
||||
self.python_min_version = deps.python_min_version
|
||||
self.python_requires = []
|
||||
for py_dep in deps.python_requires:
|
||||
if py_dep not in self.python_requires:
|
||||
try:
|
||||
__import__(py_dep)
|
||||
except ImportError:
|
||||
self.python_requires.append(py_dep)
|
||||
except (OSError, NameError, TypeError, RuntimeError) as e:
|
||||
fci.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Got an error when trying to import {}",
|
||||
).format(py_dep)
|
||||
+ ":\n"
|
||||
+ str(e)
|
||||
)
|
||||
|
||||
self.python_optional = []
|
||||
for py_dep in deps.python_optional:
|
||||
try:
|
||||
__import__(py_dep)
|
||||
except ImportError:
|
||||
self.python_optional.append(py_dep)
|
||||
except (OSError, NameError, TypeError, RuntimeError) as e:
|
||||
fci.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Got an error when trying to import {}",
|
||||
).format(py_dep)
|
||||
+ ":\n"
|
||||
+ str(e)
|
||||
)
|
||||
|
||||
self.wbs.sort()
|
||||
self.external_addons.sort()
|
||||
self.python_requires.sort()
|
||||
self.python_optional.sort()
|
||||
self.python_optional = [
|
||||
option for option in self.python_optional if option not in self.python_requires
|
||||
]
|
||||
@@ -1,143 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 The FreeCAD project association AISBL *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""The Addon Catalog is the main list of all Addons along with their various
|
||||
sources and compatible versions. Added in FreeCAD 1.1 to replace .gitmodules."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from addonmanager_metadata import Version
|
||||
from Addon import Addon
|
||||
|
||||
import addonmanager_freecad_interface as fci
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddonCatalogEntry:
|
||||
"""Each individual entry in the catalog, storing data about a particular version of an
|
||||
Addon. Note that this class needs to be identical to the one that is used in the remote cache
|
||||
generation, so don't make changes here without ensuring that the classes are synchronized."""
|
||||
|
||||
freecad_min: Optional[Version] = None
|
||||
freecad_max: Optional[Version] = None
|
||||
repository: Optional[str] = None
|
||||
git_ref: Optional[str] = None
|
||||
zip_url: Optional[str] = None
|
||||
note: Optional[str] = None
|
||||
branch_display_name: Optional[str] = None
|
||||
|
||||
def __init__(self, raw_data: Dict[str, str]) -> None:
|
||||
"""Create an AddonDictionaryEntry from the raw JSON data"""
|
||||
super().__init__()
|
||||
for key, value in raw_data.items():
|
||||
if hasattr(self, key):
|
||||
if key in ("freecad_min", "freecad_max"):
|
||||
value = Version(from_string=value)
|
||||
setattr(self, key, value)
|
||||
|
||||
def is_compatible(self) -> bool:
|
||||
"""Check whether this AddonCatalogEntry is compatible with the current version of FreeCAD"""
|
||||
if self.freecad_min is None and self.freecad_max is None:
|
||||
return True
|
||||
current_version = Version(from_list=fci.Version())
|
||||
if self.freecad_min is None:
|
||||
return current_version <= self.freecad_max
|
||||
if self.freecad_max is None:
|
||||
return current_version >= self.freecad_min
|
||||
return self.freecad_min <= current_version <= self.freecad_max
|
||||
|
||||
def unique_identifier(self) -> str:
|
||||
"""Return a unique identifier of the AddonCatalogEntry, guaranteed to be repeatable: when
|
||||
given the same basic information, the same ID is created. Used as the key when storing
|
||||
the metadata for a given AddonCatalogEntry."""
|
||||
sha256_hash = sha256()
|
||||
sha256_hash.update(str(self).encode("utf-8"))
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
class AddonCatalog:
|
||||
"""A catalog of addons grouped together into sets representing versions that are
|
||||
compatible with different versions of FreeCAD and/or represent different available branches
|
||||
of a given addon (e.g. a Development branch that users are presented)."""
|
||||
|
||||
def __init__(self, data: Dict[str, Any]):
|
||||
self._original_data = data
|
||||
self._dictionary = {}
|
||||
self._parse_raw_data()
|
||||
|
||||
def _parse_raw_data(self):
|
||||
self._dictionary = {} # Clear pre-existing contents
|
||||
for key, value in self._original_data.items():
|
||||
if key == "_meta": # Don't add the documentation object to the tree
|
||||
continue
|
||||
self._dictionary[key] = []
|
||||
for entry in value:
|
||||
self._dictionary[key].append(AddonCatalogEntry(entry))
|
||||
|
||||
def load_metadata_cache(self, cache: Dict[str, Any]):
|
||||
"""Given the raw dictionary, couple that with the remote metadata cache to create the
|
||||
final working addon dictionary. Only create Addons that are compatible with the current
|
||||
version of FreeCAD."""
|
||||
for value in self._dictionary.values():
|
||||
for entry in value:
|
||||
sha256_hash = entry.unique_identifier()
|
||||
print(sha256_hash)
|
||||
if sha256_hash in cache and entry.is_compatible():
|
||||
entry.addon = Addon.from_cache(cache[sha256_hash])
|
||||
|
||||
def get_available_addon_ids(self) -> List[str]:
|
||||
"""Get a list of IDs that have at least one entry compatible with the current version of
|
||||
FreeCAD"""
|
||||
id_list = []
|
||||
for key, value in self._dictionary.items():
|
||||
for entry in value:
|
||||
if entry.is_compatible():
|
||||
id_list.append(key)
|
||||
break
|
||||
return id_list
|
||||
|
||||
def get_available_branches(self, addon_id: str) -> List[Tuple[str, str]]:
|
||||
"""For a given ID, get the list of available branches compatible with this version of
|
||||
FreeCAD along with the branch display name. Either field may be empty, but not both. The
|
||||
first entry in the list is expected to be the "primary"."""
|
||||
if addon_id not in self._dictionary:
|
||||
return []
|
||||
result = []
|
||||
for entry in self._dictionary[addon_id]:
|
||||
if entry.is_compatible():
|
||||
result.append((entry.git_ref, entry.branch_display_name))
|
||||
return result
|
||||
|
||||
def get_addon_from_id(self, addon_id: str, branch: Optional[Tuple[str, str]] = None) -> Addon:
|
||||
"""Get the instantiated Addon object for the given ID and optionally branch. If no
|
||||
branch is provided, whichever branch is the "primary" branch will be returned (i.e. the
|
||||
first branch that matches). Raises a ValueError if no addon matches the request."""
|
||||
if addon_id not in self._dictionary:
|
||||
raise ValueError(f"Addon '{addon_id}' not found")
|
||||
for entry in self._dictionary[addon_id]:
|
||||
if not entry.is_compatible():
|
||||
continue
|
||||
if not branch or entry.branch_display_name == branch:
|
||||
return entry.addon
|
||||
raise ValueError(f"Addon '{addon_id}' has no compatible branches named '{branch}'")
|
||||
@@ -1,951 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import os
|
||||
import functools
|
||||
import tempfile
|
||||
import threading
|
||||
import json
|
||||
from datetime import date
|
||||
from typing import Dict
|
||||
|
||||
from PySide import QtGui, QtCore, QtWidgets
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
LoadPackagesFromCacheWorker,
|
||||
LoadMacrosFromCacheWorker,
|
||||
CheckWorkbenchesForUpdatesWorker,
|
||||
CacheMacroCodeWorker,
|
||||
GetBasicAddonStatsWorker,
|
||||
GetAddonScoreWorker,
|
||||
)
|
||||
from addonmanager_workers_installation import (
|
||||
UpdateMetadataCacheWorker,
|
||||
)
|
||||
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
|
||||
from addonmanager_uninstaller_gui import AddonUninstallerGUI
|
||||
from addonmanager_update_all_gui import UpdateAllGUI
|
||||
import addonmanager_utilities as utils
|
||||
import addonmanager_freecad_interface as fci
|
||||
import AddonManager_rc # pylint: disable=unused-import
|
||||
from composite_view import CompositeView
|
||||
from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
|
||||
from Widgets.addonmanager_widget_progress_bar import Progress
|
||||
from package_list import PackageListItemModel
|
||||
from Addon import Addon
|
||||
from addonmanager_python_deps_gui import (
|
||||
PythonPackageManager,
|
||||
)
|
||||
from addonmanager_cache import local_cache_needs_update
|
||||
from addonmanager_devmode import DeveloperMode
|
||||
from addonmanager_firstrun import FirstRunDialog
|
||||
from addonmanager_connection_checker import ConnectionCheckerGUI
|
||||
from addonmanager_devmode_metadata_checker import MetadataValidators
|
||||
|
||||
import NetworkManager
|
||||
|
||||
from AddonManagerOptions import AddonManagerOptions
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
def QT_TRANSLATE_NOOP(_, txt):
|
||||
return txt
|
||||
|
||||
|
||||
__title__ = "FreeCAD Addon Manager Module"
|
||||
__author__ = "Yorik van Havre", "Jonathan Wiedemann", "Kurt Kremitzki", "Chris Hennes"
|
||||
__url__ = "https://www.freecad.org"
|
||||
|
||||
"""
|
||||
FreeCAD Addon Manager Module
|
||||
|
||||
Fetches various types of addons from a variety of sources. Built-in sources are:
|
||||
* https://github.com/FreeCAD/FreeCAD-addons
|
||||
* https://github.com/FreeCAD/FreeCAD-macros
|
||||
* https://wiki.freecad.org/
|
||||
|
||||
Additional git sources may be configure via user preferences.
|
||||
|
||||
You need a working internet connection, and optionally git -- if git is not available, ZIP archives
|
||||
are downloaded instead.
|
||||
"""
|
||||
|
||||
# \defgroup ADDONMANAGER AddonManager
|
||||
# \ingroup ADDONMANAGER
|
||||
# \brief The Addon Manager allows users to install workbenches and macros made by other users
|
||||
# @{
|
||||
|
||||
INSTANCE = None
|
||||
|
||||
|
||||
def get_icon(repo: Addon, update: bool = False) -> QtGui.QIcon:
|
||||
"""Returns an icon for an Addon. Uses a cached icon if possible, unless update is True,
|
||||
in which case the icon is regenerated."""
|
||||
|
||||
if not update and repo.icon and not repo.icon.isNull() and repo.icon.isValid():
|
||||
return repo.icon
|
||||
|
||||
path = ":/icons/" + repo.name.replace(" ", "_")
|
||||
default_icon = QtGui.QIcon(":/icons/document-package.svg")
|
||||
if repo.repo_type == Addon.Kind.WORKBENCH:
|
||||
path += "_workbench_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-package.svg")
|
||||
elif repo.repo_type == Addon.Kind.MACRO:
|
||||
if repo.macro and repo.macro.icon:
|
||||
if os.path.isabs(repo.macro.icon):
|
||||
path = repo.macro.icon
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
else:
|
||||
path = os.path.join(os.path.dirname(repo.macro.src_filename), repo.macro.icon)
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
elif repo.macro and repo.macro.xpm:
|
||||
cache_path = FreeCAD.getUserCachePath()
|
||||
am_path = os.path.join(cache_path, "AddonManager", "MacroIcons")
|
||||
os.makedirs(am_path, exist_ok=True)
|
||||
path = os.path.join(am_path, repo.name + "_icon.xpm")
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as f:
|
||||
f.write(repo.macro.xpm)
|
||||
default_icon = QtGui.QIcon(repo.macro.xpm)
|
||||
else:
|
||||
path += "_macro_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
elif repo.repo_type == Addon.Kind.PACKAGE:
|
||||
# The cache might not have been downloaded yet, check to see if it's there...
|
||||
if os.path.isfile(repo.get_cached_icon_filename()):
|
||||
path = repo.get_cached_icon_filename()
|
||||
elif repo.contains_workbench():
|
||||
path += "_workbench_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-package.svg")
|
||||
elif repo.contains_macro():
|
||||
path += "_macro_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
else:
|
||||
default_icon = QtGui.QIcon(":/icons/document-package.svg")
|
||||
|
||||
if QtCore.QFile.exists(path):
|
||||
addon_icon = QtGui.QIcon(path)
|
||||
else:
|
||||
addon_icon = default_icon
|
||||
repo.icon = addon_icon
|
||||
|
||||
return addon_icon
|
||||
|
||||
|
||||
class CommandAddonManager(QtCore.QObject):
|
||||
"""The main Addon Manager class and FreeCAD command"""
|
||||
|
||||
workers = [
|
||||
"create_addon_list_worker",
|
||||
"check_worker",
|
||||
"show_worker",
|
||||
"showmacro_worker",
|
||||
"macro_worker",
|
||||
"update_metadata_cache_worker",
|
||||
"load_macro_metadata_worker",
|
||||
"update_all_worker",
|
||||
"check_for_python_package_updates_worker",
|
||||
"get_basic_addon_stats_worker",
|
||||
"get_addon_score_worker",
|
||||
]
|
||||
|
||||
lock = threading.Lock()
|
||||
restart_required = False
|
||||
|
||||
finished = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
QT_TRANSLATE_NOOP("QObject", "Addon Manager")
|
||||
FreeCADGui.addPreferencePage(
|
||||
AddonManagerOptions,
|
||||
"Addon Manager",
|
||||
)
|
||||
|
||||
self.item_model = None
|
||||
self.developer_mode = None
|
||||
self.installer_gui = None
|
||||
self.composite_view = None
|
||||
self.button_bar = None
|
||||
|
||||
self.update_cache = False
|
||||
self.dialog = None
|
||||
self.startup_sequence = []
|
||||
self.packages_with_updates = set()
|
||||
|
||||
self.macro_repo_dir = None
|
||||
self.number_of_progress_regions = 0
|
||||
self.current_progress_region = 0
|
||||
|
||||
self.check_worker = None
|
||||
self.check_for_python_package_updates_worker = None
|
||||
self.update_all_worker = None
|
||||
self.update_metadata_cache_worker = None
|
||||
self.macro_worker = None
|
||||
self.create_addon_list_worker = None
|
||||
self.get_addon_score_worker = None
|
||||
self.get_basic_addon_stats_worker = None
|
||||
self.load_macro_metadata_worker = None
|
||||
|
||||
self.macro_cache = []
|
||||
self.package_cache = {}
|
||||
self.manage_python_packages_dialog = None
|
||||
|
||||
# Set up the connection checker
|
||||
self.connection_checker = ConnectionCheckerGUI()
|
||||
self.connection_checker.connection_available.connect(self.launch)
|
||||
|
||||
# Give other parts of the AM access to the current instance
|
||||
global INSTANCE
|
||||
INSTANCE = self
|
||||
|
||||
def GetResources(self) -> Dict[str, str]:
|
||||
"""FreeCAD-required function: get the core resource information for this Mod."""
|
||||
return {
|
||||
"Pixmap": "AddonManager",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"),
|
||||
"ToolTip": QT_TRANSLATE_NOOP(
|
||||
"Std_AddonMgr",
|
||||
"Manage external workbenches, macros, and preference packs",
|
||||
),
|
||||
"Group": "Tools",
|
||||
}
|
||||
|
||||
def Activated(self) -> None:
|
||||
"""FreeCAD-required function: called when the command is activated."""
|
||||
NetworkManager.InitializeNetworkManager()
|
||||
first_run_dialog = FirstRunDialog()
|
||||
if not first_run_dialog.exec():
|
||||
return
|
||||
self.connection_checker.start()
|
||||
|
||||
def launch(self) -> None:
|
||||
"""Shows the Addon Manager UI"""
|
||||
|
||||
# create the dialog
|
||||
self.dialog = FreeCADGui.PySideUic.loadUi(
|
||||
os.path.join(os.path.dirname(__file__), "AddonManager.ui")
|
||||
)
|
||||
self.dialog.setObjectName("AddonManager_Main_Window")
|
||||
# self.dialog.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True)
|
||||
|
||||
# cleanup the leftovers from previous runs
|
||||
self.macro_repo_dir = FreeCAD.getUserMacroDir(True)
|
||||
self.packages_with_updates = set()
|
||||
self.startup_sequence = []
|
||||
self.cleanup_workers()
|
||||
self.update_cache = local_cache_needs_update()
|
||||
|
||||
# restore window geometry from stored state
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
w = pref.GetInt("WindowWidth", 800)
|
||||
h = pref.GetInt("WindowHeight", 600)
|
||||
self.composite_view = CompositeView(self.dialog)
|
||||
self.button_bar = WidgetGlobalButtonBar(self.dialog)
|
||||
|
||||
# If we are checking for updates automatically, hide the Check for updates button:
|
||||
autocheck = pref.GetBool("AutoCheck", True)
|
||||
if autocheck:
|
||||
self.button_bar.check_for_updates.hide()
|
||||
else:
|
||||
self.button_bar.update_all_addons.hide()
|
||||
|
||||
# Set up the listing of packages using the model-view-controller architecture
|
||||
self.item_model = PackageListItemModel()
|
||||
self.composite_view.setModel(self.item_model)
|
||||
self.dialog.layout().addWidget(self.composite_view)
|
||||
self.dialog.layout().addWidget(self.button_bar)
|
||||
|
||||
# set nice icons to everything, by theme with fallback to FreeCAD icons
|
||||
self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
|
||||
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
dev_mode_active = pref.GetBool("developerMode", False)
|
||||
|
||||
# enable/disable stuff
|
||||
self.button_bar.update_all_addons.setEnabled(False)
|
||||
self.hide_progress_widgets()
|
||||
self.button_bar.refresh_local_cache.setEnabled(False)
|
||||
self.button_bar.refresh_local_cache.setText(translate("AddonsInstaller", "Starting up..."))
|
||||
if dev_mode_active:
|
||||
self.button_bar.developer_tools.show()
|
||||
else:
|
||||
self.button_bar.developer_tools.hide()
|
||||
|
||||
# connect slots
|
||||
self.dialog.rejected.connect(self.reject)
|
||||
self.dialog.accepted.connect(self.accept)
|
||||
self.button_bar.update_all_addons.clicked.connect(self.update_all)
|
||||
self.button_bar.close.clicked.connect(self.dialog.reject)
|
||||
self.button_bar.refresh_local_cache.clicked.connect(self.on_button_update_cache_clicked)
|
||||
self.button_bar.check_for_updates.clicked.connect(
|
||||
lambda: self.force_check_updates(standalone=True)
|
||||
)
|
||||
self.button_bar.python_dependencies.clicked.connect(self.show_python_updates_dialog)
|
||||
self.button_bar.developer_tools.clicked.connect(self.show_developer_tools)
|
||||
self.composite_view.package_list.stop_loading.connect(self.stop_update)
|
||||
self.composite_view.package_list.setEnabled(False)
|
||||
self.composite_view.execute.connect(self.execute_macro)
|
||||
self.composite_view.install.connect(self.launch_installer_gui)
|
||||
self.composite_view.uninstall.connect(self.remove)
|
||||
self.composite_view.update.connect(self.update)
|
||||
self.composite_view.update_status.connect(self.status_updated)
|
||||
|
||||
# center the dialog over the FreeCAD window
|
||||
self.dialog.resize(w, h)
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
self.dialog.move(
|
||||
mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center()
|
||||
)
|
||||
|
||||
# begin populating the table in a set of sub-threads
|
||||
self.startup()
|
||||
|
||||
# rock 'n roll!!!
|
||||
self.dialog.exec()
|
||||
|
||||
def cleanup_workers(self) -> None:
|
||||
"""Ensure that no workers are running by explicitly asking them to stop and waiting for
|
||||
them until they do"""
|
||||
for worker in self.workers:
|
||||
if hasattr(self, worker):
|
||||
thread = getattr(self, worker)
|
||||
if thread:
|
||||
if not thread.isFinished():
|
||||
thread.blockSignals(True)
|
||||
thread.requestInterruption()
|
||||
for worker in self.workers:
|
||||
if hasattr(self, worker):
|
||||
thread = getattr(self, worker)
|
||||
if thread:
|
||||
if not thread.isFinished():
|
||||
finished = thread.wait(500)
|
||||
if not finished:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Worker process {} is taking a long time to stop...",
|
||||
).format(worker)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
def accept(self) -> None:
|
||||
self.finished.emit()
|
||||
|
||||
def reject(self) -> None:
|
||||
"""called when the window has been closed"""
|
||||
|
||||
# save window geometry for next use
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetInt("WindowWidth", self.dialog.width())
|
||||
pref.SetInt("WindowHeight", self.dialog.height())
|
||||
|
||||
# ensure all threads are finished before closing
|
||||
ok_to_close = True
|
||||
worker_killed = False
|
||||
self.startup_sequence = []
|
||||
for worker in self.workers:
|
||||
if hasattr(self, worker):
|
||||
thread = getattr(self, worker)
|
||||
if thread:
|
||||
if not thread.isFinished():
|
||||
thread.blockSignals(True)
|
||||
thread.requestInterruption()
|
||||
worker_killed = True
|
||||
ok_to_close = False
|
||||
while not ok_to_close:
|
||||
ok_to_close = True
|
||||
for worker in self.workers:
|
||||
if hasattr(self, worker):
|
||||
thread = getattr(self, worker)
|
||||
if thread:
|
||||
thread.wait(25)
|
||||
if not thread.isFinished():
|
||||
ok_to_close = False
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
# Write the cache data if it's safe to do so:
|
||||
if not worker_killed:
|
||||
for repo in self.item_model.repos:
|
||||
if repo.repo_type == Addon.Kind.MACRO:
|
||||
self.cache_macro(repo)
|
||||
else:
|
||||
self.cache_package(repo)
|
||||
self.write_package_cache()
|
||||
self.write_macro_cache()
|
||||
else:
|
||||
self.write_cache_stopfile()
|
||||
FreeCAD.Console.PrintLog(
|
||||
"Not writing the cache because a process was forcibly terminated and the state is "
|
||||
"unknown.\n"
|
||||
)
|
||||
|
||||
if self.restart_required:
|
||||
# display restart dialog
|
||||
m = QtWidgets.QMessageBox()
|
||||
m.setWindowTitle(translate("AddonsInstaller", "Addon manager"))
|
||||
m.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
|
||||
m.setText(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"You must restart FreeCAD for changes to take effect.",
|
||||
)
|
||||
)
|
||||
m.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
m.setStandardButtons(
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
| QtWidgets.QMessageBox.StandardButton.Cancel
|
||||
)
|
||||
m.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Cancel)
|
||||
ok_btn = m.button(QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
cancel_btn = m.button(QtWidgets.QMessageBox.StandardButton.Cancel)
|
||||
ok_btn.setText(translate("AddonsInstaller", "Restart now"))
|
||||
cancel_btn.setText(translate("AddonsInstaller", "Restart later"))
|
||||
ret = m.exec_()
|
||||
if ret == QtWidgets.QMessageBox.StandardButton.Ok:
|
||||
# restart FreeCAD after a delay to give time to this dialog to close
|
||||
QtCore.QTimer.singleShot(1000, utils.restart_freecad)
|
||||
|
||||
self.finished.emit()
|
||||
|
||||
def startup(self) -> None:
|
||||
"""Downloads the available packages listings and populates the table
|
||||
|
||||
This proceeds in four stages: first, the main GitHub repository is queried for a list of
|
||||
possible addons. Each addon is specified as a git submodule with name and branch
|
||||
information. The actual specific commit ID of the submodule (as listed on GitHub) is
|
||||
ignored. Any extra repositories specified by the user are appended to this list.
|
||||
|
||||
Second, the list of macros is downloaded from the FreeCAD/FreeCAD-macros repository and
|
||||
the wiki.
|
||||
|
||||
Third, each of these items is queried for a package.xml metadata file. If that file exists
|
||||
it is downloaded, cached, and any icons that it references are also downloaded and cached.
|
||||
|
||||
Finally, for workbenches that are not contained within a package (e.g. they provide no
|
||||
metadata), an additional git query is made to see if an update is available. Macros are
|
||||
checked for file changes.
|
||||
|
||||
Each of these stages is launched in a separate thread to ensure that the UI remains
|
||||
responsive, and the operation can be cancelled.
|
||||
|
||||
Each stage is also subject to caching, so may return immediately, if no cache update has
|
||||
been requested."""
|
||||
|
||||
# Each function in this list is expected to launch a thread and connect its completion
|
||||
# signal to self.do_next_startup_phase, or to shortcut to calling
|
||||
# self.do_next_startup_phase if it is not launching a worker
|
||||
self.startup_sequence = [
|
||||
self.populate_packages_table,
|
||||
self.activate_table_widgets,
|
||||
self.populate_macros,
|
||||
self.update_metadata_cache,
|
||||
self.check_updates,
|
||||
self.check_python_updates,
|
||||
self.fetch_addon_stats,
|
||||
self.fetch_addon_score,
|
||||
self.select_addon,
|
||||
]
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
if pref.GetBool("DownloadMacros", True):
|
||||
self.startup_sequence.append(self.load_macro_metadata)
|
||||
self.number_of_progress_regions = len(self.startup_sequence)
|
||||
self.current_progress_region = 0
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def do_next_startup_phase(self) -> None:
|
||||
"""Pop the top item in self.startup_sequence off the list and run it"""
|
||||
|
||||
if len(self.startup_sequence) > 0:
|
||||
phase_runner = self.startup_sequence.pop(0)
|
||||
self.current_progress_region += 1
|
||||
phase_runner()
|
||||
else:
|
||||
self.hide_progress_widgets()
|
||||
self.update_cache = False
|
||||
self.button_bar.refresh_local_cache.setEnabled(True)
|
||||
self.button_bar.refresh_local_cache.setText(
|
||||
translate("AddonsInstaller", "Refresh local cache")
|
||||
)
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetString("LastCacheUpdate", date.today().isoformat())
|
||||
self.composite_view.package_list.item_filter.invalidateFilter()
|
||||
|
||||
def populate_packages_table(self) -> None:
|
||||
self.item_model.clear()
|
||||
|
||||
use_cache = not self.update_cache
|
||||
if use_cache:
|
||||
if os.path.isfile(utils.get_cache_file_name("package_cache.json")):
|
||||
with open(utils.get_cache_file_name("package_cache.json"), encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
try:
|
||||
from_json = json.loads(data)
|
||||
if len(from_json) == 0:
|
||||
use_cache = False
|
||||
except json.JSONDecodeError:
|
||||
use_cache = False
|
||||
else:
|
||||
use_cache = False
|
||||
|
||||
if not use_cache:
|
||||
self.update_cache = True # Make sure to trigger the other cache updates, if the json
|
||||
# file was missing
|
||||
self.create_addon_list_worker = CreateAddonListWorker()
|
||||
self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
|
||||
self.update_progress_bar(translate("AddonsInstaller", "Creating addon list"), 10, 100)
|
||||
self.create_addon_list_worker.finished.connect(
|
||||
self.do_next_startup_phase
|
||||
) # Link to step 2
|
||||
self.create_addon_list_worker.start()
|
||||
else:
|
||||
self.create_addon_list_worker = LoadPackagesFromCacheWorker(
|
||||
utils.get_cache_file_name("package_cache.json")
|
||||
)
|
||||
self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
|
||||
self.update_progress_bar(translate("AddonsInstaller", "Loading addon list"), 10, 100)
|
||||
self.create_addon_list_worker.finished.connect(
|
||||
self.do_next_startup_phase
|
||||
) # Link to step 2
|
||||
self.create_addon_list_worker.start()
|
||||
|
||||
def cache_package(self, repo: Addon):
|
||||
if not hasattr(self, "package_cache"):
|
||||
self.package_cache = {}
|
||||
self.package_cache[repo.name] = repo.to_cache()
|
||||
|
||||
def write_package_cache(self):
|
||||
if hasattr(self, "package_cache"):
|
||||
package_cache_path = utils.get_cache_file_name("package_cache.json")
|
||||
with open(package_cache_path, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(self.package_cache, indent=" "))
|
||||
|
||||
def activate_table_widgets(self) -> None:
|
||||
self.composite_view.package_list.setEnabled(True)
|
||||
self.composite_view.package_list.ui.view_bar.search.setFocus()
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def populate_macros(self) -> None:
|
||||
macro_cache_file = utils.get_cache_file_name("macro_cache.json")
|
||||
cache_is_bad = True
|
||||
if os.path.isfile(macro_cache_file):
|
||||
size = os.path.getsize(macro_cache_file)
|
||||
if size > 1000: # Make sure there is actually data in there
|
||||
cache_is_bad = False
|
||||
if cache_is_bad:
|
||||
if not self.update_cache:
|
||||
self.update_cache = True # Make sure to trigger the other cache updates, if the
|
||||
# json file was missing
|
||||
self.create_addon_list_worker = CreateAddonListWorker()
|
||||
self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
|
||||
self.update_progress_bar(
|
||||
translate("AddonsInstaller", "Creating macro list"), 10, 100
|
||||
)
|
||||
self.create_addon_list_worker.finished.connect(
|
||||
self.do_next_startup_phase
|
||||
) # Link to step 2
|
||||
self.create_addon_list_worker.start()
|
||||
else:
|
||||
# It's already been done in the previous step (TODO: Refactor to eliminate this
|
||||
# step)
|
||||
self.do_next_startup_phase()
|
||||
else:
|
||||
self.macro_worker = LoadMacrosFromCacheWorker(
|
||||
utils.get_cache_file_name("macro_cache.json")
|
||||
)
|
||||
self.macro_worker.add_macro_signal.connect(self.add_addon_repo)
|
||||
self.macro_worker.finished.connect(self.do_next_startup_phase)
|
||||
self.macro_worker.start()
|
||||
|
||||
def cache_macro(self, repo: Addon):
|
||||
if not hasattr(self, "macro_cache"):
|
||||
self.macro_cache = []
|
||||
if repo.macro is not None:
|
||||
self.macro_cache.append(repo.macro.to_cache())
|
||||
else:
|
||||
FreeCAD.Console.PrintError(
|
||||
f"Addon Manager: Internal error, cache_macro called on non-macro {repo.name}\n"
|
||||
)
|
||||
|
||||
def write_macro_cache(self):
|
||||
if not hasattr(self, "macro_cache"):
|
||||
return
|
||||
macro_cache_path = utils.get_cache_file_name("macro_cache.json")
|
||||
with open(macro_cache_path, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(self.macro_cache, indent=" "))
|
||||
self.macro_cache = []
|
||||
|
||||
def update_metadata_cache(self) -> None:
|
||||
if self.update_cache:
|
||||
self.update_metadata_cache_worker = UpdateMetadataCacheWorker(self.item_model.repos)
|
||||
self.update_metadata_cache_worker.finished.connect(
|
||||
self.do_next_startup_phase
|
||||
) # Link to step 4
|
||||
self.update_metadata_cache_worker.progress_made.connect(self.update_progress_bar)
|
||||
self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
|
||||
self.update_metadata_cache_worker.start()
|
||||
else:
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def on_button_update_cache_clicked(self) -> None:
|
||||
self.update_cache = True
|
||||
cache_path = FreeCAD.getUserCachePath()
|
||||
am_path = os.path.join(cache_path, "AddonManager")
|
||||
utils.rmdir(am_path)
|
||||
self.button_bar.refresh_local_cache.setEnabled(False)
|
||||
self.button_bar.refresh_local_cache.setText(
|
||||
translate("AddonsInstaller", "Updating cache...")
|
||||
)
|
||||
self.startup()
|
||||
|
||||
# Re-caching implies checking for updates, regardless of the user's autocheck option
|
||||
if self.check_updates in self.startup_sequence:
|
||||
self.startup_sequence.remove(self.check_updates)
|
||||
self.startup_sequence.append(self.force_check_updates)
|
||||
|
||||
def on_package_updated(self, repo: Addon) -> None:
|
||||
"""Called when the named package has either new metadata or a new icon (or both)"""
|
||||
|
||||
with self.lock:
|
||||
repo.icon = get_icon(repo, update=True)
|
||||
self.item_model.reload_item(repo)
|
||||
|
||||
def load_macro_metadata(self) -> None:
|
||||
if self.update_cache:
|
||||
self.load_macro_metadata_worker = CacheMacroCodeWorker(self.item_model.repos)
|
||||
self.load_macro_metadata_worker.update_macro.connect(self.on_package_updated)
|
||||
self.load_macro_metadata_worker.progress_made.connect(self.update_progress_bar)
|
||||
self.load_macro_metadata_worker.finished.connect(self.do_next_startup_phase)
|
||||
self.load_macro_metadata_worker.start()
|
||||
else:
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def select_addon(self) -> None:
|
||||
prefs = fci.Preferences()
|
||||
selection = prefs.get("SelectedAddon")
|
||||
if selection:
|
||||
self.composite_view.package_list.select_addon(selection)
|
||||
prefs.set("SelectedAddon", "")
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def check_updates(self) -> None:
|
||||
"""checks every installed addon for available updates"""
|
||||
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
autocheck = pref.GetBool("AutoCheck", True)
|
||||
if not autocheck:
|
||||
FreeCAD.Console.PrintLog(
|
||||
"Addon Manager: Skipping update check because AutoCheck user preference is False\n"
|
||||
)
|
||||
self.do_next_startup_phase()
|
||||
return
|
||||
if not self.packages_with_updates:
|
||||
self.force_check_updates(standalone=False)
|
||||
else:
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def force_check_updates(self, standalone=False) -> None:
|
||||
if hasattr(self, "check_worker"):
|
||||
thread = self.check_worker
|
||||
if thread:
|
||||
if not thread.isFinished():
|
||||
self.do_next_startup_phase()
|
||||
return
|
||||
|
||||
self.button_bar.update_all_addons.setText(
|
||||
translate("AddonsInstaller", "Checking for updates...")
|
||||
)
|
||||
self.packages_with_updates.clear()
|
||||
self.button_bar.update_all_addons.show()
|
||||
self.button_bar.check_for_updates.setDisabled(True)
|
||||
self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos)
|
||||
self.check_worker.finished.connect(self.do_next_startup_phase)
|
||||
self.check_worker.finished.connect(self.update_check_complete)
|
||||
self.check_worker.progress_made.connect(self.update_progress_bar)
|
||||
if standalone:
|
||||
self.current_progress_region = 1
|
||||
self.number_of_progress_regions = 1
|
||||
self.check_worker.update_status.connect(self.status_updated)
|
||||
self.check_worker.start()
|
||||
self.enable_updates(len(self.packages_with_updates))
|
||||
|
||||
def status_updated(self, repo: Addon) -> None:
|
||||
self.item_model.reload_item(repo)
|
||||
if repo.status() == Addon.Status.UPDATE_AVAILABLE:
|
||||
self.packages_with_updates.add(repo)
|
||||
self.enable_updates(len(self.packages_with_updates))
|
||||
elif repo.status() == Addon.Status.PENDING_RESTART:
|
||||
self.restart_required = True
|
||||
|
||||
def enable_updates(self, number_of_updates: int) -> None:
|
||||
"""enables the update button"""
|
||||
|
||||
if number_of_updates:
|
||||
self.button_bar.set_number_of_available_updates(number_of_updates)
|
||||
elif (
|
||||
hasattr(self, "check_worker")
|
||||
and self.check_worker is not None
|
||||
and self.check_worker.isRunning()
|
||||
):
|
||||
self.button_bar.update_all_addons.setText(
|
||||
translate("AddonsInstaller", "Checking for updates...")
|
||||
)
|
||||
else:
|
||||
self.button_bar.set_number_of_available_updates(0)
|
||||
|
||||
def update_check_complete(self) -> None:
|
||||
self.enable_updates(len(self.packages_with_updates))
|
||||
self.button_bar.check_for_updates.setEnabled(True)
|
||||
|
||||
def check_python_updates(self) -> None:
|
||||
PythonPackageManager.migrate_old_am_installations() # Migrate 0.20 to 0.21
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def show_python_updates_dialog(self) -> None:
|
||||
if not self.manage_python_packages_dialog:
|
||||
self.manage_python_packages_dialog = PythonPackageManager(self.item_model.repos)
|
||||
self.manage_python_packages_dialog.show()
|
||||
|
||||
def fetch_addon_stats(self) -> None:
|
||||
"""Fetch the Addon Stats JSON data from a URL"""
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
url = pref.GetString("AddonsStatsURL", "https://freecad.org/addon_stats.json")
|
||||
if url and url != "NONE":
|
||||
self.get_basic_addon_stats_worker = GetBasicAddonStatsWorker(
|
||||
url, self.item_model.repos, self.dialog
|
||||
)
|
||||
self.get_basic_addon_stats_worker.finished.connect(self.do_next_startup_phase)
|
||||
self.get_basic_addon_stats_worker.update_addon_stats.connect(self.update_addon_stats)
|
||||
self.get_basic_addon_stats_worker.start()
|
||||
else:
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def update_addon_stats(self, addon: Addon):
|
||||
self.item_model.reload_item(addon)
|
||||
|
||||
def fetch_addon_score(self) -> None:
|
||||
"""Fetch the Addon score JSON data from a URL"""
|
||||
prefs = fci.Preferences()
|
||||
url = prefs.get("AddonsScoreURL")
|
||||
if url and url != "NONE":
|
||||
self.get_addon_score_worker = GetAddonScoreWorker(
|
||||
url, self.item_model.repos, self.dialog
|
||||
)
|
||||
self.get_addon_score_worker.finished.connect(self.score_fetched_successfully)
|
||||
self.get_addon_score_worker.finished.connect(self.do_next_startup_phase)
|
||||
self.get_addon_score_worker.update_addon_score.connect(self.update_addon_score)
|
||||
self.get_addon_score_worker.start()
|
||||
else:
|
||||
self.composite_view.package_list.ui.view_bar.set_rankings_available(False)
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def update_addon_score(self, addon: Addon):
|
||||
self.item_model.reload_item(addon)
|
||||
|
||||
def score_fetched_successfully(self):
|
||||
self.composite_view.package_list.ui.view_bar.set_rankings_available(True)
|
||||
|
||||
def show_developer_tools(self) -> None:
|
||||
"""Display the developer tools dialog"""
|
||||
if not self.developer_mode:
|
||||
self.developer_mode = DeveloperMode()
|
||||
self.developer_mode.show()
|
||||
|
||||
checker = MetadataValidators()
|
||||
checker.validate_all(self.item_model.repos)
|
||||
|
||||
def add_addon_repo(self, addon_repo: Addon) -> None:
|
||||
"""adds a workbench to the list"""
|
||||
|
||||
if addon_repo.icon is None or addon_repo.icon.isNull():
|
||||
addon_repo.icon = get_icon(addon_repo)
|
||||
for repo in self.item_model.repos:
|
||||
if repo.name == addon_repo.name:
|
||||
# self.item_model.reload_item(repo) # If we want to have later additions superseded
|
||||
# earlier
|
||||
return
|
||||
self.item_model.append_item(addon_repo)
|
||||
|
||||
def append_to_repos_list(self, repo: Addon) -> None:
|
||||
"""this function allows threads to update the main list of workbenches"""
|
||||
self.item_model.append_item(repo)
|
||||
|
||||
def update(self, repo: Addon) -> None:
|
||||
self.launch_installer_gui(repo)
|
||||
|
||||
def mark_repo_update_available(self, repo: Addon, available: bool) -> None:
|
||||
if available:
|
||||
repo.set_status(Addon.Status.UPDATE_AVAILABLE)
|
||||
else:
|
||||
repo.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
|
||||
self.item_model.reload_item(repo)
|
||||
self.composite_view.package_details_controller.show_repo(repo)
|
||||
|
||||
def launch_installer_gui(self, addon: Addon) -> None:
|
||||
if self.installer_gui is not None:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Cannot launch a new installer until the previous one has finished.",
|
||||
)
|
||||
)
|
||||
return
|
||||
if addon.macro is not None:
|
||||
self.installer_gui = MacroInstallerGUI(addon)
|
||||
else:
|
||||
self.installer_gui = AddonInstallerGUI(addon, self.item_model.repos)
|
||||
self.installer_gui.success.connect(self.on_package_status_changed)
|
||||
self.installer_gui.finished.connect(self.cleanup_installer)
|
||||
self.installer_gui.run() # Does not block
|
||||
|
||||
def cleanup_installer(self) -> None:
|
||||
QtCore.QTimer.singleShot(500, self.no_really_clean_up_the_installer)
|
||||
|
||||
def no_really_clean_up_the_installer(self) -> None:
|
||||
self.installer_gui = None
|
||||
|
||||
def update_all(self) -> None:
|
||||
"""Asynchronously apply all available updates: individual failures are noted, but do not
|
||||
stop other updates"""
|
||||
|
||||
if self.installer_gui is not None:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Cannot launch a new installer until the previous one has finished.",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.installer_gui = UpdateAllGUI(self.item_model.repos)
|
||||
self.installer_gui.addon_updated.connect(self.on_package_status_changed)
|
||||
self.installer_gui.finished.connect(self.cleanup_installer)
|
||||
self.installer_gui.run() # Does not block
|
||||
|
||||
def hide_progress_widgets(self) -> None:
|
||||
"""hides the progress bar and related widgets"""
|
||||
self.composite_view.package_list.set_loading(False)
|
||||
|
||||
def show_progress_widgets(self) -> None:
|
||||
self.composite_view.package_list.set_loading(True)
|
||||
|
||||
def update_progress_bar(self, message: str, current_value: int, max_value: int) -> None:
|
||||
"""Update the progress bar, showing it if it's hidden"""
|
||||
|
||||
max_value = max_value if max_value > 0 else 1
|
||||
|
||||
if current_value < 0:
|
||||
current_value = 0
|
||||
elif current_value > max_value:
|
||||
current_value = max_value
|
||||
|
||||
self.show_progress_widgets()
|
||||
|
||||
progress = Progress(
|
||||
status_text=message,
|
||||
number_of_tasks=self.number_of_progress_regions,
|
||||
current_task=self.current_progress_region - 1,
|
||||
current_task_progress=current_value / max_value,
|
||||
)
|
||||
self.composite_view.package_list.update_loading_progress(progress)
|
||||
|
||||
def stop_update(self) -> None:
|
||||
self.cleanup_workers()
|
||||
self.hide_progress_widgets()
|
||||
self.write_cache_stopfile()
|
||||
self.button_bar.refresh_local_cache.setEnabled(True)
|
||||
self.button_bar.refresh_local_cache.setText(
|
||||
translate("AddonsInstaller", "Refresh local cache")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def write_cache_stopfile() -> None:
|
||||
stopfile = utils.get_cache_file_name("CACHE_UPDATE_INTERRUPTED")
|
||||
with open(stopfile, "w", encoding="utf8") as f:
|
||||
f.write(
|
||||
"This file indicates that a cache operation was interrupted, and "
|
||||
"the cache is in an unknown state. It will be deleted next time "
|
||||
"AddonManager re-caches."
|
||||
)
|
||||
|
||||
def on_package_status_changed(self, repo: Addon) -> None:
|
||||
if repo.status() == Addon.Status.PENDING_RESTART:
|
||||
self.restart_required = True
|
||||
self.item_model.reload_item(repo)
|
||||
self.composite_view.package_details_controller.show_repo(repo)
|
||||
if repo in self.packages_with_updates:
|
||||
self.packages_with_updates.remove(repo)
|
||||
self.enable_updates(len(self.packages_with_updates))
|
||||
|
||||
def execute_macro(self, repo: Addon) -> None:
|
||||
"""executes a selected macro"""
|
||||
|
||||
macro = repo.macro
|
||||
if not macro or not macro.code:
|
||||
return
|
||||
|
||||
if macro.is_installed():
|
||||
macro_path = os.path.join(self.macro_repo_dir, macro.filename)
|
||||
FreeCADGui.open(str(macro_path))
|
||||
self.dialog.hide()
|
||||
FreeCADGui.SendMsgToActiveView("Run")
|
||||
else:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_install_succeeded = macro.install(temp_dir)
|
||||
if not temp_install_succeeded:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate("AddonsInstaller", "Temporary installation of macro failed.")
|
||||
)
|
||||
return
|
||||
macro_path = os.path.join(temp_dir, macro.filename)
|
||||
FreeCADGui.open(str(macro_path))
|
||||
self.dialog.hide()
|
||||
FreeCADGui.SendMsgToActiveView("Run")
|
||||
|
||||
def remove(self, addon: Addon) -> None:
|
||||
"""Remove this addon."""
|
||||
if self.installer_gui is not None:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Cannot launch a new installer until the previous one has finished.",
|
||||
)
|
||||
)
|
||||
return
|
||||
self.installer_gui = AddonUninstallerGUI(addon)
|
||||
self.installer_gui.finished.connect(self.cleanup_installer)
|
||||
self.installer_gui.finished.connect(
|
||||
functools.partial(self.on_package_status_changed, addon)
|
||||
)
|
||||
self.installer_gui.run() # Does not block
|
||||
|
||||
|
||||
# @}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>928</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Addon Manager</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4"/>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,314 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Contains a the Addon Manager's preferences dialog management class"""
|
||||
|
||||
import os
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
from PySide import QtCore
|
||||
from PySide.QtGui import QIcon
|
||||
from PySide.QtWidgets import (
|
||||
QWidget,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QHeaderView,
|
||||
QRadioButton,
|
||||
QLineEdit,
|
||||
QTextEdit,
|
||||
)
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class AddonManagerOptions:
|
||||
"""A class containing a form element that is inserted as a FreeCAD preference page."""
|
||||
|
||||
def __init__(self, _=None):
|
||||
self.form = FreeCADGui.PySideUic.loadUi(
|
||||
os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui")
|
||||
)
|
||||
self.table_model = CustomRepoDataModel()
|
||||
self.form.customRepositoriesTableView.setModel(self.table_model)
|
||||
|
||||
self.form.addCustomRepositoryButton.setIcon(
|
||||
QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
|
||||
)
|
||||
self.form.removeCustomRepositoryButton.setIcon(
|
||||
QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
|
||||
)
|
||||
|
||||
self.form.customRepositoriesTableView.horizontalHeader().setStretchLastSection(False)
|
||||
self.form.customRepositoriesTableView.horizontalHeader().setSectionResizeMode(
|
||||
0, QHeaderView.Stretch
|
||||
)
|
||||
self.form.customRepositoriesTableView.horizontalHeader().setSectionResizeMode(
|
||||
1, QHeaderView.ResizeToContents
|
||||
)
|
||||
|
||||
self.form.addCustomRepositoryButton.clicked.connect(self._add_custom_repo_clicked)
|
||||
self.form.removeCustomRepositoryButton.clicked.connect(self._remove_custom_repo_clicked)
|
||||
self.form.customRepositoriesTableView.doubleClicked.connect(self._row_double_clicked)
|
||||
|
||||
def saveSettings(self):
|
||||
"""Required function: called by the preferences dialog when Apply or Save is clicked,
|
||||
saves out the preference data by reading it from the widgets."""
|
||||
for widget in self.form.children():
|
||||
self.recursive_widget_saver(widget)
|
||||
self.table_model.save_model()
|
||||
|
||||
def recursive_widget_saver(self, widget):
|
||||
"""Writes out the data for this widget and all of its children, recursively."""
|
||||
if isinstance(widget, QWidget):
|
||||
# See if it's one of ours:
|
||||
pref_path = widget.property("prefPath")
|
||||
pref_entry = widget.property("prefEntry")
|
||||
if pref_path and pref_entry:
|
||||
pref_path = pref_path.data()
|
||||
pref_entry = pref_entry.data()
|
||||
pref_access_string = f"User parameter:BaseApp/Preferences/{str(pref_path,'utf-8')}"
|
||||
pref = FreeCAD.ParamGet(pref_access_string)
|
||||
if isinstance(widget, QCheckBox):
|
||||
checked = widget.isChecked()
|
||||
pref.SetBool(str(pref_entry, "utf-8"), checked)
|
||||
elif isinstance(widget, QRadioButton):
|
||||
checked = widget.isChecked()
|
||||
pref.SetBool(str(pref_entry, "utf-8"), checked)
|
||||
elif isinstance(widget, QComboBox):
|
||||
new_index = widget.currentIndex()
|
||||
pref.SetInt(str(pref_entry, "utf-8"), new_index)
|
||||
elif isinstance(widget, QTextEdit):
|
||||
text = widget.toPlainText()
|
||||
pref.SetString(str(pref_entry, "utf-8"), text)
|
||||
elif isinstance(widget, QLineEdit):
|
||||
text = widget.text()
|
||||
pref.SetString(str(pref_entry, "utf-8"), text)
|
||||
elif widget.metaObject().className() == "Gui::PrefFileChooser":
|
||||
filename = str(widget.property("fileName"))
|
||||
filename = pref.SetString(str(pref_entry, "utf-8"), filename)
|
||||
|
||||
# Recurse over children
|
||||
if isinstance(widget, QtCore.QObject):
|
||||
for child in widget.children():
|
||||
self.recursive_widget_saver(child)
|
||||
|
||||
def loadSettings(self):
|
||||
"""Required function: called by the preferences dialog when it is launched,
|
||||
loads the preference data and assigns it to the widgets."""
|
||||
for widget in self.form.children():
|
||||
self.recursive_widget_loader(widget)
|
||||
self.table_model.load_model()
|
||||
|
||||
def recursive_widget_loader(self, widget):
|
||||
"""Loads the data for this widget and all of its children, recursively."""
|
||||
if isinstance(widget, QWidget):
|
||||
# See if it's one of ours:
|
||||
pref_path = widget.property("prefPath")
|
||||
pref_entry = widget.property("prefEntry")
|
||||
if pref_path and pref_entry:
|
||||
pref_path = pref_path.data()
|
||||
pref_entry = pref_entry.data()
|
||||
pref_access_string = f"User parameter:BaseApp/Preferences/{str(pref_path,'utf-8')}"
|
||||
pref = FreeCAD.ParamGet(pref_access_string)
|
||||
if isinstance(widget, QCheckBox):
|
||||
widget.setChecked(pref.GetBool(str(pref_entry, "utf-8")))
|
||||
elif isinstance(widget, QRadioButton):
|
||||
if pref.GetBool(str(pref_entry, "utf-8")):
|
||||
widget.setChecked(True)
|
||||
elif isinstance(widget, QComboBox):
|
||||
new_index = pref.GetInt(str(pref_entry, "utf-8"))
|
||||
widget.setCurrentIndex(new_index)
|
||||
elif isinstance(widget, QTextEdit):
|
||||
text = pref.GetString(str(pref_entry, "utf-8"))
|
||||
widget.setText(text)
|
||||
elif isinstance(widget, QLineEdit):
|
||||
text = pref.GetString(str(pref_entry, "utf-8"))
|
||||
widget.setText(text)
|
||||
elif widget.metaObject().className() == "Gui::PrefFileChooser":
|
||||
filename = pref.GetString(str(pref_entry, "utf-8"))
|
||||
widget.setProperty("fileName", filename)
|
||||
|
||||
# Recurse over children
|
||||
if isinstance(widget, QtCore.QObject):
|
||||
for child in widget.children():
|
||||
self.recursive_widget_loader(child)
|
||||
|
||||
def _add_custom_repo_clicked(self):
|
||||
"""Callback: show the Add custom repo dialog"""
|
||||
dlg = CustomRepositoryDialog()
|
||||
url, branch = dlg.exec()
|
||||
if url and branch:
|
||||
self.table_model.appendData(url, branch)
|
||||
|
||||
def _remove_custom_repo_clicked(self):
|
||||
"""Callback: when the remove button is clicked, get the current selection and remove it."""
|
||||
item = self.form.customRepositoriesTableView.currentIndex()
|
||||
if not item.isValid():
|
||||
return
|
||||
row = item.row()
|
||||
self.table_model.removeRows(row, 1, QtCore.QModelIndex())
|
||||
|
||||
def _row_double_clicked(self, item):
|
||||
"""Edit the row that was double-clicked"""
|
||||
row = item.row()
|
||||
dlg = CustomRepositoryDialog()
|
||||
url_index = self.table_model.createIndex(row, 0)
|
||||
branch_index = self.table_model.createIndex(row, 1)
|
||||
dlg.dialog.urlLineEdit.setText(self.table_model.data(url_index))
|
||||
dlg.dialog.branchLineEdit.setText(self.table_model.data(branch_index))
|
||||
url, branch = dlg.exec()
|
||||
if url and branch:
|
||||
self.table_model.setData(url_index, url)
|
||||
self.table_model.setData(branch_index, branch)
|
||||
|
||||
|
||||
class CustomRepoDataModel(QtCore.QAbstractTableModel):
|
||||
"""The model for the custom repositories: wraps the underlying preference data and uses that
|
||||
as its main data store."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
pref_access_string = "User parameter:BaseApp/Preferences/Addons"
|
||||
self.pref = FreeCAD.ParamGet(pref_access_string)
|
||||
self.load_model()
|
||||
|
||||
def load_model(self):
|
||||
"""Load the data from the preferences entry"""
|
||||
pref_entry: str = self.pref.GetString("CustomRepositories", "")
|
||||
|
||||
# The entry is saved as a space- and newline-delimited text block: break it into its
|
||||
# constituent parts
|
||||
lines = pref_entry.split("\n")
|
||||
self.model = []
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
split_data = line.split()
|
||||
if len(split_data) > 1:
|
||||
branch = split_data[1]
|
||||
else:
|
||||
branch = "master"
|
||||
url = split_data[0]
|
||||
self.model.append([url, branch])
|
||||
|
||||
def save_model(self):
|
||||
"""Save the data into a preferences entry"""
|
||||
entry = ""
|
||||
for row in self.model:
|
||||
entry += f"{row[0]} {row[1]}\n"
|
||||
self.pref.SetString("CustomRepositories", entry)
|
||||
|
||||
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
"""The number of rows"""
|
||||
if parent.isValid():
|
||||
return 0
|
||||
return len(self.model)
|
||||
|
||||
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
"""The number of columns (which is always 2)"""
|
||||
if parent.isValid():
|
||||
return 0
|
||||
return 2
|
||||
|
||||
def data(self, index, role=QtCore.Qt.DisplayRole):
|
||||
"""The data at an index."""
|
||||
if role != QtCore.Qt.DisplayRole:
|
||||
return None
|
||||
row = index.row()
|
||||
column = index.column()
|
||||
if row > len(self.model):
|
||||
return None
|
||||
if column > 1:
|
||||
return None
|
||||
return self.model[row][column]
|
||||
|
||||
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
|
||||
"""Get the row and column header data."""
|
||||
if role != QtCore.Qt.DisplayRole:
|
||||
return None
|
||||
if orientation == QtCore.Qt.Vertical:
|
||||
return section + 1
|
||||
if section == 0:
|
||||
return translate(
|
||||
"AddonsInstaller",
|
||||
"Repository URL",
|
||||
"Preferences header for custom repositories",
|
||||
)
|
||||
if section == 1:
|
||||
return translate(
|
||||
"AddonsInstaller",
|
||||
"Branch name",
|
||||
"Preferences header for custom repositories",
|
||||
)
|
||||
return None
|
||||
|
||||
def removeRows(self, row, count, parent):
|
||||
"""Remove rows"""
|
||||
self.beginRemoveRows(parent, row, row + count - 1)
|
||||
for _ in range(count):
|
||||
self.model.pop(row)
|
||||
self.endRemoveRows()
|
||||
|
||||
def insertRows(self, row, count, parent):
|
||||
"""Insert blank rows"""
|
||||
self.beginInsertRows(parent, row, row + count - 1)
|
||||
for _ in range(count):
|
||||
self.model.insert(["", ""])
|
||||
self.endInsertRows()
|
||||
|
||||
def appendData(self, url, branch):
|
||||
"""Append this url and branch to the end of the list"""
|
||||
row = self.rowCount()
|
||||
self.beginInsertRows(QtCore.QModelIndex(), row, row)
|
||||
self.model.append([url, branch])
|
||||
self.endInsertRows()
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
"""Set the data at this index"""
|
||||
if role != QtCore.Qt.EditRole:
|
||||
return
|
||||
self.model[index.row()][index.column()] = value
|
||||
self.dataChanged.emit(index, index)
|
||||
|
||||
|
||||
class CustomRepositoryDialog:
|
||||
"""A dialog for setting up a custom repository, with branch information"""
|
||||
|
||||
def __init__(self):
|
||||
self.dialog = FreeCADGui.PySideUic.loadUi(
|
||||
os.path.join(os.path.dirname(__file__), "AddonManagerOptions_AddCustomRepository.ui")
|
||||
)
|
||||
|
||||
def exec(self):
|
||||
"""Run the dialog modally, and return either None or a tuple or (url,branch)"""
|
||||
result = self.dialog.exec()
|
||||
if result == QDialog.Accepted:
|
||||
url = self.dialog.urlLineEdit.text()
|
||||
branch = self.dialog.branchLineEdit.text()
|
||||
return (url, branch)
|
||||
return (None, None)
|
||||
@@ -1,482 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Gui::Dialog::DlgSettingsAddonManager</class>
|
||||
<widget class="QWidget" name="Gui::Dialog::DlgSettingsAddonManager">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>757</width>
|
||||
<height>783</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Addon manager options</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxcheckupdates">
|
||||
<property name="toolTip">
|
||||
<string>If this option is selected, when launching the Addon Manager,
|
||||
installed addons will be checked for available updates</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Automatically check for updates at start (requires Git)</string>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>AutoCheck</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxdownloadmacros">
|
||||
<property name="text">
|
||||
<string>Download Macro metadata (approximately 10MB)</string>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>DownloadMacros</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Cache update frequency</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefComboBox" name="guiprefcomboboxupdatefrequency">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>UpdateFrequencyComboEntry</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Manual (no automatic updates)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Daily</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Weekly</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhideunlicensed">
|
||||
<property name="text">
|
||||
<string>Hide Addons without a license</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideUnlicensed</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidenonfsf">
|
||||
<property name="text">
|
||||
<string>Hide Addons with non-FSF Free/Libre license</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideNonFSFFreeLibre</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxnonosi">
|
||||
<property name="text">
|
||||
<string>Hide Addons with non-OSI-approved license</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideNonOSIApproved</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidepy2">
|
||||
<property name="text">
|
||||
<string>Hide Addons marked Python 2 Only</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HidePy2</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhideobsolete">
|
||||
<property name="text">
|
||||
<string>Hide Addons marked Obsolete</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideObsolete</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidenewerfreecadrequired">
|
||||
<property name="text">
|
||||
<string>Hide Addons that require a newer version of FreeCAD</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideNewerFreeCADRequired</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Custom repositories</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="customRepositoriesTableView">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="addCustomRepositoryButton">
|
||||
<property name="text">
|
||||
<string notr="true">...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="removeCustomRepositoryButton">
|
||||
<property name="text">
|
||||
<string notr="true">...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Proxy</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="Gui::PrefRadioButton" name="guiprefradiobuttonnoproxy">
|
||||
<property name="text">
|
||||
<string>No proxy</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>NoProxyCheck</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefRadioButton" name="guiprefradiobuttonsystemproxy">
|
||||
<property name="text">
|
||||
<string>User system proxy</string>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>SystemProxyCheck</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefRadioButton" name="guiprefradiobuttonuserproxy">
|
||||
<property name="text">
|
||||
<string>User-defined proxy:</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>UserProxyCheck</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefLineEdit" name="guipreflineedituserproxy">
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>ProxyUrl</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayoutScore">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_score">
|
||||
<property name="text">
|
||||
<string>Score source URL</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefLineEdit" name="guipreflineeditscoresourceurl">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>The URL for the Addon Score data (see Addon Manager wiki page for formatting and hosting details).</string>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>AddonsScoreURL</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Path to Git executable (optional):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="Gui::PrefFileChooser" name="gui::preffilechooser" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>The path to the Git executable. Autodetected if needed and not specified.</string>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>GitExecutable</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="advanced">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Advanced Options</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxShowBranchSwitcher">
|
||||
<property name="text">
|
||||
<string>Show option to change branches (requires Git)</string>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>ShowBranchSwitcher</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxDisableGit">
|
||||
<property name="text">
|
||||
<string>Disable Git (fall back to ZIP downloads only)</string>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>disableGit</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxDeveloperMode">
|
||||
<property name="toolTip">
|
||||
<string>Activate Addon Manager options intended for developers of new Addons.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Addon developer mode</string>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>developerMode</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Gui::PrefCheckBox</class>
|
||||
<extends>QCheckBox</extends>
|
||||
<header>Gui/PrefWidgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Gui::PrefComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>Gui/PrefWidgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Gui::PrefRadioButton</class>
|
||||
<extends>QRadioButton</extends>
|
||||
<header>Gui/PrefWidgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Gui::PrefLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>Gui/PrefWidgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Gui::PrefFileChooser</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>Gui/PrefWidgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,84 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AddCustomRepositoryDialog</class>
|
||||
<widget class="QDialog" name="AddCustomRepositoryDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>95</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Custom repository</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Repository URL</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="urlLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Branch</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="3">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="branchLineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>AddCustomRepositoryDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>AddCustomRepositoryDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -1,463 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Mock objects for use when testing the addon manager non-GUI code."""
|
||||
|
||||
# pylint: disable=too-few-public-methods,too-many-instance-attributes,missing-function-docstring
|
||||
|
||||
import os
|
||||
from typing import Union, List
|
||||
import xml.etree.ElementTree as ElemTree
|
||||
|
||||
|
||||
class GitFailed(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class MockConsole:
|
||||
"""Spy for the FreeCAD.Console -- does NOT print anything out, just logs it."""
|
||||
|
||||
def __init__(self):
|
||||
self.log = []
|
||||
self.messages = []
|
||||
self.warnings = []
|
||||
self.errors = []
|
||||
|
||||
def PrintLog(self, data: str):
|
||||
self.log.append(data)
|
||||
|
||||
def PrintMessage(self, data: str):
|
||||
self.messages.append(data)
|
||||
|
||||
def PrintWarning(self, data: str):
|
||||
self.warnings.append(data)
|
||||
|
||||
def PrintError(self, data: str):
|
||||
self.errors.append(data)
|
||||
|
||||
def missing_newlines(self) -> int:
|
||||
"""In most cases, all console entries should end with newlines: this is a
|
||||
convenience function for unit testing that is true."""
|
||||
counter = 0
|
||||
counter += self._count_missing_newlines(self.log)
|
||||
counter += self._count_missing_newlines(self.messages)
|
||||
counter += self._count_missing_newlines(self.warnings)
|
||||
counter += self._count_missing_newlines(self.errors)
|
||||
return counter
|
||||
|
||||
@staticmethod
|
||||
def _count_missing_newlines(some_list) -> int:
|
||||
counter = 0
|
||||
for line in some_list:
|
||||
if line[-1] != "\n":
|
||||
counter += 1
|
||||
return counter
|
||||
|
||||
|
||||
class MockAddon:
|
||||
"""Minimal Addon class"""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = None,
|
||||
url: str = None,
|
||||
status: object = None,
|
||||
branch: str = "main",
|
||||
):
|
||||
test_dir = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||
if name:
|
||||
self.name = name
|
||||
self.display_name = name
|
||||
else:
|
||||
self.name = "MockAddon"
|
||||
self.display_name = "Mock Addon"
|
||||
self.url = url if url else os.path.join(test_dir, "test_simple_repo.zip")
|
||||
self.branch = branch
|
||||
self.status = status
|
||||
self.macro = None
|
||||
self.update_status = None
|
||||
self.metadata = None
|
||||
self.icon_file = None
|
||||
self.last_updated = None
|
||||
self.requires = set()
|
||||
self.python_requires = set()
|
||||
self.python_optional = set()
|
||||
self.on_git = False
|
||||
self.on_wiki = True
|
||||
|
||||
def set_status(self, status):
|
||||
self.update_status = status
|
||||
|
||||
@staticmethod
|
||||
def get_best_icon_relative_path():
|
||||
return ""
|
||||
|
||||
|
||||
class MockMacro:
|
||||
"""Minimal Macro class"""
|
||||
|
||||
def __init__(self, name="MockMacro"):
|
||||
self.name = name
|
||||
self.filename = self.name + ".FCMacro"
|
||||
self.icon = "" # If set, should just be fake filename, doesn't have to exist
|
||||
self.xpm = ""
|
||||
self.code = ""
|
||||
self.raw_code_url = ""
|
||||
self.other_files = [] # If set, should be fake names, don't have to exist
|
||||
self.details_filled_from_file = False
|
||||
self.details_filled_from_code = False
|
||||
self.parsed_wiki_page = False
|
||||
self.on_git = False
|
||||
self.on_wiki = True
|
||||
|
||||
def install(self, location: os.PathLike):
|
||||
"""Installer function for the mock macro object: creates a file with the src_filename
|
||||
attribute, and optionally an icon, xpm, and other_files. The data contained in these files
|
||||
is not usable and serves only as a placeholder for the existence of the files.
|
||||
"""
|
||||
|
||||
with open(
|
||||
os.path.join(location, self.filename),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
f.write("Test file for macro installation unit tests")
|
||||
if self.icon:
|
||||
with open(os.path.join(location, self.icon), "wb") as f:
|
||||
f.write(b"Fake icon data - nothing to see here\n")
|
||||
if self.xpm:
|
||||
with open(os.path.join(location, "MockMacro_icon.xpm"), "w", encoding="utf-8") as f:
|
||||
f.write(self.xpm)
|
||||
for name in self.other_files:
|
||||
if "/" in name:
|
||||
new_location = os.path.dirname(os.path.join(location, name))
|
||||
os.makedirs(new_location, exist_ok=True)
|
||||
with open(os.path.join(location, name), "w", encoding="utf-8") as f:
|
||||
f.write("# Fake macro data for unit testing\n")
|
||||
return True, []
|
||||
|
||||
def fill_details_from_file(self, _):
|
||||
"""Tracks that this function was called, but otherwise does nothing"""
|
||||
self.details_filled_from_file = True
|
||||
|
||||
def fill_details_from_code(self, _):
|
||||
self.details_filled_from_code = True
|
||||
|
||||
def parse_wiki_page(self, _):
|
||||
self.parsed_wiki_page = True
|
||||
|
||||
|
||||
class SignalCatcher:
|
||||
"""Object to track signals that it has caught.
|
||||
|
||||
Usage:
|
||||
catcher = SignalCatcher()
|
||||
my_signal.connect(catcher.catch_signal)
|
||||
do_things_that_emit_the_signal()
|
||||
self.assertTrue(catcher.caught)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.caught = False
|
||||
self.killed = False
|
||||
self.args = None
|
||||
|
||||
def catch_signal(self, *args):
|
||||
self.caught = True
|
||||
self.args = args
|
||||
|
||||
def die(self):
|
||||
self.killed = True
|
||||
|
||||
|
||||
class AddonSignalCatcher:
|
||||
"""Signal catcher specifically designed for catching emitted addons."""
|
||||
|
||||
def __init__(self):
|
||||
self.addons = []
|
||||
|
||||
def catch_signal(self, addon):
|
||||
self.addons.append(addon)
|
||||
|
||||
|
||||
class CallCatcher:
|
||||
"""Generic call monitor -- use to override functions that are not themselves under
|
||||
test so that you can detect when the function has been called, and how many times.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.call_count = 0
|
||||
self.args = None
|
||||
|
||||
def catch_call(self, *args):
|
||||
self.called = True
|
||||
self.call_count += 1
|
||||
self.args = args
|
||||
|
||||
|
||||
class MockGitManager:
|
||||
"""A mock git manager: does NOT require a git installation. Takes no actions, only records
|
||||
which functions are called for instrumentation purposes. Can be forced to appear to fail as
|
||||
needed. Various member variables can be set to emulate necessary return responses.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.called_methods = []
|
||||
self.update_available_response = False
|
||||
self.current_tag_response = "main"
|
||||
self.current_branch_response = "main"
|
||||
self.get_remote_response = "No remote set"
|
||||
self.get_branches_response = ["main"]
|
||||
self.get_last_committers_response = {"John Doe": {"email": "jdoe@freecad.org", "count": 1}}
|
||||
self.get_last_authors_response = {"Jane Doe": {"email": "jdoe@freecad.org", "count": 1}}
|
||||
self.should_fail = False
|
||||
self.fail_once = False # Switch back to success after the simulated failure
|
||||
|
||||
def _check_for_failure(self):
|
||||
if self.should_fail:
|
||||
if self.fail_once:
|
||||
self.should_fail = False
|
||||
raise GitFailed("Unit test forced failure")
|
||||
|
||||
def clone(self, _remote, _local_path, _args: List[str] = None):
|
||||
self.called_methods.append("clone")
|
||||
self._check_for_failure()
|
||||
|
||||
def async_clone(self, _remote, _local_path, _progress_monitor, _args: List[str] = None):
|
||||
self.called_methods.append("async_clone")
|
||||
self._check_for_failure()
|
||||
|
||||
def checkout(self, _local_path, _spec, _args: List[str] = None):
|
||||
self.called_methods.append("checkout")
|
||||
self._check_for_failure()
|
||||
|
||||
def update(self, _local_path):
|
||||
self.called_methods.append("update")
|
||||
self._check_for_failure()
|
||||
|
||||
def status(self, _local_path) -> str:
|
||||
self.called_methods.append("status")
|
||||
self._check_for_failure()
|
||||
return "Up-to-date"
|
||||
|
||||
def reset(self, _local_path, _args: List[str] = None):
|
||||
self.called_methods.append("reset")
|
||||
self._check_for_failure()
|
||||
|
||||
def async_fetch_and_update(self, _local_path, _progress_monitor, _args=None):
|
||||
self.called_methods.append("async_fetch_and_update")
|
||||
self._check_for_failure()
|
||||
|
||||
def update_available(self, _local_path) -> bool:
|
||||
self.called_methods.append("update_available")
|
||||
self._check_for_failure()
|
||||
return self.update_available_response
|
||||
|
||||
def current_tag(self, _local_path) -> str:
|
||||
self.called_methods.append("current_tag")
|
||||
self._check_for_failure()
|
||||
return self.current_tag_response
|
||||
|
||||
def current_branch(self, _local_path) -> str:
|
||||
self.called_methods.append("current_branch")
|
||||
self._check_for_failure()
|
||||
return self.current_branch_response
|
||||
|
||||
def repair(self, _remote, _local_path):
|
||||
self.called_methods.append("repair")
|
||||
self._check_for_failure()
|
||||
|
||||
def get_remote(self, _local_path) -> str:
|
||||
self.called_methods.append("get_remote")
|
||||
self._check_for_failure()
|
||||
return self.get_remote_response
|
||||
|
||||
def get_branches(self, _local_path) -> List[str]:
|
||||
self.called_methods.append("get_branches")
|
||||
self._check_for_failure()
|
||||
return self.get_branches_response
|
||||
|
||||
def get_last_committers(self, _local_path, _n=10):
|
||||
self.called_methods.append("get_last_committers")
|
||||
self._check_for_failure()
|
||||
return self.get_last_committers_response
|
||||
|
||||
def get_last_authors(self, _local_path, _n=10):
|
||||
self.called_methods.append("get_last_authors")
|
||||
self._check_for_failure()
|
||||
return self.get_last_authors_response
|
||||
|
||||
|
||||
class MockSignal:
|
||||
"""A purely synchronous signal, instrumented and intended only for use in unit testing.
|
||||
emit() is semi-functional, but does not use queued slots so cannot be used across
|
||||
threads."""
|
||||
|
||||
def __init__(self, *args):
|
||||
self.expected_types = args
|
||||
self.connections = []
|
||||
self.disconnections = []
|
||||
self.emitted = False
|
||||
|
||||
def connect(self, func):
|
||||
self.connections.append(func)
|
||||
|
||||
def disconnect(self, func):
|
||||
if func in self.connections:
|
||||
self.connections.remove(func)
|
||||
self.disconnections.append(func)
|
||||
|
||||
def emit(self, *args):
|
||||
self.emitted = True
|
||||
for connection in self.connections:
|
||||
connection(args)
|
||||
|
||||
|
||||
class MockNetworkManager:
|
||||
"""Instrumented mock for the NetworkManager. Does no network access, is not asynchronous, and
|
||||
does not require a running event loop. No submitted requests ever complete."""
|
||||
|
||||
def __init__(self):
|
||||
self.urls = []
|
||||
self.aborted = []
|
||||
self.data = MockByteArray()
|
||||
self.called_methods = []
|
||||
|
||||
self.completed = MockSignal(int, int, MockByteArray)
|
||||
self.progress_made = MockSignal(int, int, int)
|
||||
self.progress_complete = MockSignal(int, int, os.PathLike)
|
||||
|
||||
def submit_unmonitored_get(self, url: str) -> int:
|
||||
self.urls.append(url)
|
||||
self.called_methods.append("submit_unmonitored_get")
|
||||
return len(self.urls) - 1
|
||||
|
||||
def submit_monitored_get(self, url: str) -> int:
|
||||
self.urls.append(url)
|
||||
self.called_methods.append("submit_monitored_get")
|
||||
return len(self.urls) - 1
|
||||
|
||||
def blocking_get(self, url: str):
|
||||
self.urls.append(url)
|
||||
self.called_methods.append("blocking_get")
|
||||
return self.data
|
||||
|
||||
def abort_all(self):
|
||||
self.called_methods.append("abort_all")
|
||||
for url in self.urls:
|
||||
self.aborted.append(url)
|
||||
|
||||
def abort(self, index: int):
|
||||
self.called_methods.append("abort")
|
||||
self.aborted.append(self.urls[index])
|
||||
|
||||
|
||||
class MockByteArray:
|
||||
"""Mock for QByteArray. Only provides the data() access member."""
|
||||
|
||||
def __init__(self, data_to_wrap="data".encode("utf-8")):
|
||||
self.wrapped = data_to_wrap
|
||||
|
||||
def data(self) -> bytes:
|
||||
return self.wrapped
|
||||
|
||||
|
||||
class MockThread:
|
||||
"""Mock for QThread for use when threading is not being used, but interruption
|
||||
needs to be tested. Set interrupt_after_n_calls to the call number to stop at."""
|
||||
|
||||
def __init__(self):
|
||||
self.interrupt_after_n_calls = 0
|
||||
self.interrupt_check_counter = 0
|
||||
|
||||
def isInterruptionRequested(self):
|
||||
self.interrupt_check_counter += 1
|
||||
if (
|
||||
self.interrupt_after_n_calls
|
||||
and self.interrupt_check_counter >= self.interrupt_after_n_calls
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MockPref:
|
||||
def __init__(self):
|
||||
self.prefs = {}
|
||||
self.pref_set_counter = {}
|
||||
self.pref_get_counter = {}
|
||||
|
||||
def set_prefs(self, pref_dict: dict) -> None:
|
||||
self.prefs = pref_dict
|
||||
|
||||
def GetInt(self, key: str, default: int) -> int:
|
||||
return self.Get(key, default)
|
||||
|
||||
def GetString(self, key: str, default: str) -> str:
|
||||
return self.Get(key, default)
|
||||
|
||||
def GetBool(self, key: str, default: bool) -> bool:
|
||||
return self.Get(key, default)
|
||||
|
||||
def Get(self, key: str, default):
|
||||
if key not in self.pref_set_counter:
|
||||
self.pref_get_counter[key] = 1
|
||||
else:
|
||||
self.pref_get_counter[key] += 1
|
||||
if key in self.prefs:
|
||||
return self.prefs[key]
|
||||
raise ValueError(f"Expected key not in mock preferences: {key}")
|
||||
|
||||
def SetInt(self, key: str, value: int) -> None:
|
||||
return self.Set(key, value)
|
||||
|
||||
def SetString(self, key: str, value: str) -> None:
|
||||
return self.Set(key, value)
|
||||
|
||||
def SetBool(self, key: str, value: bool) -> None:
|
||||
return self.Set(key, value)
|
||||
|
||||
def Set(self, key: str, value):
|
||||
if key not in self.pref_set_counter:
|
||||
self.pref_set_counter[key] = 1
|
||||
else:
|
||||
self.pref_set_counter[key] += 1
|
||||
self.prefs[key] = value
|
||||
|
||||
|
||||
class MockExists:
|
||||
def __init__(self, files: List[str] = None):
|
||||
"""Returns True for all files in files, and False for all others"""
|
||||
self.files = files
|
||||
self.files_checked = []
|
||||
|
||||
def exists(self, check_file: str):
|
||||
self.files_checked.append(check_file)
|
||||
if not self.files:
|
||||
return False
|
||||
for file in self.files:
|
||||
if check_file.endswith(file):
|
||||
return True
|
||||
return False
|
||||
@@ -1,407 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import tempfile
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append("../../")
|
||||
|
||||
from Addon import Addon, INTERNAL_WORKBENCHES
|
||||
from addonmanager_macro import Macro
|
||||
|
||||
|
||||
class TestAddon(unittest.TestCase):
|
||||
MODULE = "test_addon" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||
|
||||
def test_display_name(self):
|
||||
# Case 1: No display name set elsewhere: name == display_name
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
self.assertEqual(addon.name, "FreeCAD")
|
||||
self.assertEqual(addon.display_name, "FreeCAD")
|
||||
|
||||
# Case 2: Package.xml metadata file sets a display name:
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "good_package.xml"))
|
||||
self.assertEqual(addon.name, "FreeCAD")
|
||||
self.assertEqual(addon.display_name, "Test Workbench")
|
||||
|
||||
def test_git_url_cleanup(self):
|
||||
base_url = "https://github.com/FreeCAD/FreeCAD"
|
||||
test_urls = [f" {base_url} ", f"{base_url}.git", f" {base_url}.git "]
|
||||
for url in test_urls:
|
||||
addon = Addon("FreeCAD", url, Addon.Status.NOT_INSTALLED, "master")
|
||||
self.assertEqual(addon.url, base_url)
|
||||
|
||||
def test_tag_extraction(self):
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "good_package.xml"))
|
||||
|
||||
tags = addon.tags
|
||||
self.assertEqual(len(tags), 5)
|
||||
expected_tags = set()
|
||||
expected_tags.add("Tag0")
|
||||
expected_tags.add("Tag1")
|
||||
expected_tags.add("TagA")
|
||||
expected_tags.add("TagB")
|
||||
expected_tags.add("TagC")
|
||||
self.assertEqual(expected_tags, tags)
|
||||
|
||||
def test_contains_functions(self):
|
||||
# Test package.xml combinations:
|
||||
|
||||
# Workbenches
|
||||
addon_with_workbench = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_workbench.load_metadata_file(os.path.join(self.test_dir, "workbench_only.xml"))
|
||||
self.assertTrue(addon_with_workbench.contains_workbench())
|
||||
self.assertFalse(addon_with_workbench.contains_macro())
|
||||
self.assertFalse(addon_with_workbench.contains_preference_pack())
|
||||
self.assertFalse(addon_with_workbench.contains_bundle())
|
||||
self.assertFalse(addon_with_workbench.contains_other())
|
||||
|
||||
# Macros
|
||||
addon_with_macro = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_macro.load_metadata_file(os.path.join(self.test_dir, "macro_only.xml"))
|
||||
self.assertFalse(addon_with_macro.contains_workbench())
|
||||
self.assertTrue(addon_with_macro.contains_macro())
|
||||
self.assertFalse(addon_with_macro.contains_preference_pack())
|
||||
self.assertFalse(addon_with_workbench.contains_bundle())
|
||||
self.assertFalse(addon_with_workbench.contains_other())
|
||||
|
||||
# Preference Packs
|
||||
addon_with_prefpack = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_prefpack.load_metadata_file(os.path.join(self.test_dir, "prefpack_only.xml"))
|
||||
self.assertFalse(addon_with_prefpack.contains_workbench())
|
||||
self.assertFalse(addon_with_prefpack.contains_macro())
|
||||
self.assertTrue(addon_with_prefpack.contains_preference_pack())
|
||||
self.assertFalse(addon_with_workbench.contains_bundle())
|
||||
self.assertFalse(addon_with_workbench.contains_other())
|
||||
|
||||
# Combination
|
||||
addon_with_all = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_all.load_metadata_file(os.path.join(self.test_dir, "combination.xml"))
|
||||
self.assertTrue(addon_with_all.contains_workbench())
|
||||
self.assertTrue(addon_with_all.contains_macro())
|
||||
self.assertTrue(addon_with_all.contains_preference_pack())
|
||||
self.assertTrue(addon_with_all.contains_bundle())
|
||||
self.assertTrue(addon_with_all.contains_other())
|
||||
|
||||
# Now do the simple, explicitly-set cases
|
||||
addon_wb = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_wb.repo_type = Addon.Kind.WORKBENCH
|
||||
self.assertTrue(addon_wb.contains_workbench())
|
||||
self.assertFalse(addon_wb.contains_macro())
|
||||
self.assertFalse(addon_wb.contains_preference_pack())
|
||||
|
||||
addon_m = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_m.repo_type = Addon.Kind.MACRO
|
||||
self.assertFalse(addon_m.contains_workbench())
|
||||
self.assertTrue(addon_m.contains_macro())
|
||||
self.assertFalse(addon_m.contains_preference_pack())
|
||||
|
||||
# There is no equivalent for preference packs, they are always accompanied by a
|
||||
# metadata file
|
||||
|
||||
def test_create_from_macro(self):
|
||||
macro_file = os.path.join(self.test_dir, "DoNothing.FCMacro")
|
||||
macro = Macro("DoNothing")
|
||||
macro.fill_details_from_file(macro_file)
|
||||
addon = Addon.from_macro(macro)
|
||||
|
||||
self.assertEqual(addon.repo_type, Addon.Kind.MACRO)
|
||||
self.assertEqual(addon.name, "DoNothing")
|
||||
self.assertEqual(
|
||||
addon.macro.comment,
|
||||
"Do absolutely nothing. For Addon Manager integration tests.",
|
||||
)
|
||||
self.assertEqual(addon.url, "https://github.com/FreeCAD/FreeCAD")
|
||||
self.assertEqual(addon.macro.version, "1.0")
|
||||
self.assertEqual(len(addon.macro.other_files), 3)
|
||||
self.assertEqual(addon.macro.author, "Chris Hennes")
|
||||
self.assertEqual(addon.macro.date, "2022-02-28")
|
||||
self.assertEqual(addon.macro.icon, "not_real.png")
|
||||
self.assertNotEqual(addon.macro.xpm, "")
|
||||
|
||||
def test_cache(self):
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
cache_data = addon.to_cache()
|
||||
second_addon = Addon.from_cache(cache_data)
|
||||
|
||||
self.assertTrue(addon.__dict__, second_addon.__dict__)
|
||||
|
||||
def test_dependency_resolution(self):
|
||||
addonA = Addon(
|
||||
"AddonA",
|
||||
"https://github.com/FreeCAD/FakeAddonA",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addonB = Addon(
|
||||
"AddonB",
|
||||
"https://github.com/FreeCAD/FakeAddonB",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addonC = Addon(
|
||||
"AddonC",
|
||||
"https://github.com/FreeCAD/FakeAddonC",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addonD = Addon(
|
||||
"AddonD",
|
||||
"https://github.com/FreeCAD/FakeAddonD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
|
||||
addonA.requires.add("AddonB")
|
||||
addonB.requires.add("AddonC")
|
||||
addonB.requires.add("AddonD")
|
||||
addonD.requires.add("CAM")
|
||||
|
||||
all_addons = {
|
||||
addonA.name: addonA,
|
||||
addonB.name: addonB,
|
||||
addonC.name: addonC,
|
||||
addonD.name: addonD,
|
||||
}
|
||||
|
||||
deps = Addon.Dependencies()
|
||||
addonA.walk_dependency_tree(all_addons, deps)
|
||||
|
||||
self.assertEqual(len(deps.required_external_addons), 3)
|
||||
addon_strings = [addon.name for addon in deps.required_external_addons]
|
||||
self.assertTrue(
|
||||
"AddonB" in addon_strings,
|
||||
"AddonB not in required dependencies, and it should be.",
|
||||
)
|
||||
self.assertTrue(
|
||||
"AddonC" in addon_strings,
|
||||
"AddonC not in required dependencies, and it should be.",
|
||||
)
|
||||
self.assertTrue(
|
||||
"AddonD" in addon_strings,
|
||||
"AddonD not in required dependencies, and it should be.",
|
||||
)
|
||||
self.assertTrue(
|
||||
"CAM" in deps.internal_workbenches,
|
||||
"CAM not in workbench dependencies, and it should be.",
|
||||
)
|
||||
|
||||
def test_internal_workbench_list(self):
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "depends_on_all_workbenches.xml"))
|
||||
deps = Addon.Dependencies()
|
||||
addon.walk_dependency_tree({}, deps)
|
||||
self.assertEqual(len(deps.internal_workbenches), len(INTERNAL_WORKBENCHES))
|
||||
|
||||
def test_version_check(self):
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "test_version_detection.xml"))
|
||||
|
||||
self.assertEqual(
|
||||
len(addon.tags),
|
||||
1,
|
||||
"Wrong number of tags found: version requirements should have restricted to only one",
|
||||
)
|
||||
self.assertFalse(
|
||||
"TagA" in addon.tags,
|
||||
"Found 'TagA' in tags, it should have been excluded by version requirement",
|
||||
)
|
||||
self.assertTrue(
|
||||
"TagB" in addon.tags,
|
||||
"Failed to find 'TagB' in tags, it should have been included",
|
||||
)
|
||||
self.assertFalse(
|
||||
"TagC" in addon.tags,
|
||||
"Found 'TagA' in tags, it should have been excluded by version requirement",
|
||||
)
|
||||
|
||||
def test_try_find_wbname_in_files_empty_dir(self):
|
||||
with tempfile.TemporaryDirectory() as mod_dir:
|
||||
# Arrange
|
||||
test_addon = Addon("test")
|
||||
test_addon.mod_directory = mod_dir
|
||||
os.mkdir(os.path.join(mod_dir, test_addon.name))
|
||||
|
||||
# Act
|
||||
wb_name = test_addon.try_find_wbname_in_files()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(wb_name, "")
|
||||
|
||||
def test_try_find_wbname_in_files_non_python_ignored(self):
|
||||
with tempfile.TemporaryDirectory() as mod_dir:
|
||||
# Arrange
|
||||
test_addon = Addon("test")
|
||||
test_addon.mod_directory = mod_dir
|
||||
base_path = os.path.join(mod_dir, test_addon.name)
|
||||
os.mkdir(base_path)
|
||||
file_path = os.path.join(base_path, "test.txt")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write("Gui.addWorkbench(TestWorkbench())")
|
||||
|
||||
# Act
|
||||
wb_name = test_addon.try_find_wbname_in_files()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(wb_name, "")
|
||||
|
||||
def test_try_find_wbname_in_files_simple(self):
|
||||
with tempfile.TemporaryDirectory() as mod_dir:
|
||||
# Arrange
|
||||
test_addon = Addon("test")
|
||||
test_addon.mod_directory = mod_dir
|
||||
base_path = os.path.join(mod_dir, test_addon.name)
|
||||
os.mkdir(base_path)
|
||||
file_path = os.path.join(base_path, "test.py")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write("Gui.addWorkbench(TestWorkbench())")
|
||||
|
||||
# Act
|
||||
wb_name = test_addon.try_find_wbname_in_files()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(wb_name, "TestWorkbench")
|
||||
|
||||
def test_try_find_wbname_in_files_subdir(self):
|
||||
with tempfile.TemporaryDirectory() as mod_dir:
|
||||
# Arrange
|
||||
test_addon = Addon("test")
|
||||
test_addon.mod_directory = mod_dir
|
||||
base_path = os.path.join(mod_dir, test_addon.name)
|
||||
os.mkdir(base_path)
|
||||
subdir = os.path.join(base_path, "subdirectory")
|
||||
os.mkdir(subdir)
|
||||
file_path = os.path.join(subdir, "test.py")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write("Gui.addWorkbench(TestWorkbench())")
|
||||
|
||||
# Act
|
||||
wb_name = test_addon.try_find_wbname_in_files()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(wb_name, "TestWorkbench")
|
||||
|
||||
def test_try_find_wbname_in_files_variable_used(self):
|
||||
with tempfile.TemporaryDirectory() as mod_dir:
|
||||
# Arrange
|
||||
test_addon = Addon("test")
|
||||
test_addon.mod_directory = mod_dir
|
||||
base_path = os.path.join(mod_dir, test_addon.name)
|
||||
os.mkdir(base_path)
|
||||
file_path = os.path.join(base_path, "test.py")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write("Gui.addWorkbench(wb)")
|
||||
|
||||
# Act
|
||||
wb_name = test_addon.try_find_wbname_in_files()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(wb_name, "")
|
||||
|
||||
def test_try_find_wbname_in_files_variants(self):
|
||||
variants = [
|
||||
"Gui.addWorkbench(TestWorkbench())",
|
||||
"Gui.addWorkbench (TestWorkbench())",
|
||||
"Gui.addWorkbench( TestWorkbench() )",
|
||||
"Gui.addWorkbench(TestWorkbench( ))",
|
||||
"Gui.addWorkbench( TestWorkbench( ) )",
|
||||
"Gui.addWorkbench( TestWorkbench ( ) )",
|
||||
"Gui.addWorkbench ( TestWorkbench ( ) )",
|
||||
]
|
||||
for variant in variants:
|
||||
with self.subTest(variant=variant):
|
||||
with tempfile.TemporaryDirectory() as mod_dir:
|
||||
# Arrange
|
||||
test_addon = Addon("test")
|
||||
test_addon.mod_directory = mod_dir
|
||||
base_path = os.path.join(mod_dir, test_addon.name)
|
||||
os.mkdir(base_path)
|
||||
file_path = os.path.join(base_path, "test.py")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(variant)
|
||||
|
||||
# Act
|
||||
wb_name = test_addon.try_find_wbname_in_files()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(wb_name, "TestWorkbench")
|
||||
@@ -1,213 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
# pylint: disable=global-at-module-level,global-statement,import-outside-toplevel,
|
||||
|
||||
"""Tests for the AddonCatalog and AddonCatalogEntry classes."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
global AddonCatalogEntry
|
||||
global AddonCatalog
|
||||
global Version
|
||||
|
||||
|
||||
class TestAddonCatalogEntry(unittest.TestCase):
|
||||
"""Tests for the AddonCatalogEntry class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Start mock for addonmanager_licenses class."""
|
||||
global AddonCatalogEntry
|
||||
global AddonCatalog
|
||||
global Version
|
||||
self.addon_patch = mock.patch.dict("sys.modules", {"addonmanager_licenses": mock.Mock()})
|
||||
self.mock_addon_module = self.addon_patch.start()
|
||||
from AddonCatalog import AddonCatalogEntry, AddonCatalog
|
||||
from addonmanager_metadata import Version
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop patching the addonmanager_licenses class"""
|
||||
self.addon_patch.stop()
|
||||
|
||||
def test_version_match_without_restrictions(self):
|
||||
"""Given an AddonCatalogEntry that has no version restrictions, a fixed version matches."""
|
||||
with patch("addonmanager_freecad_interface.Version") as mock_freecad:
|
||||
mock_freecad.Version = lambda: (1, 2, 3, "dev")
|
||||
ac = AddonCatalogEntry({})
|
||||
self.assertTrue(ac.is_compatible())
|
||||
|
||||
def test_version_match_with_min_no_max_good_match(self):
|
||||
"""Given an AddonCatalogEntry with a minimum FreeCAD version, a version smaller than that
|
||||
does not match."""
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=(1, 2, 3, "dev")):
|
||||
ac = AddonCatalogEntry({"freecad_min": Version(from_string="1.2")})
|
||||
self.assertTrue(ac.is_compatible())
|
||||
|
||||
def test_version_match_with_max_no_min_good_match(self):
|
||||
"""Given an AddonCatalogEntry with a maximum FreeCAD version, a version larger than that
|
||||
does not match."""
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=(1, 2, 3, "dev")):
|
||||
ac = AddonCatalogEntry({"freecad_max": Version(from_string="1.3")})
|
||||
self.assertTrue(ac.is_compatible())
|
||||
|
||||
def test_version_match_with_min_and_max_good_match(self):
|
||||
"""Given an AddonCatalogEntry with both a minimum and maximum FreeCAD version, a version
|
||||
between the two matches."""
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=(1, 2, 3, "dev")):
|
||||
ac = AddonCatalogEntry(
|
||||
{
|
||||
"freecad_min": Version(from_string="1.1"),
|
||||
"freecad_max": Version(from_string="1.3"),
|
||||
}
|
||||
)
|
||||
self.assertTrue(ac.is_compatible())
|
||||
|
||||
def test_version_match_with_min_and_max_bad_match_high(self):
|
||||
"""Given an AddonCatalogEntry with both a minimum and maximum FreeCAD version, a version
|
||||
higher than the maximum does not match."""
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=(1, 3, 3, "dev")):
|
||||
ac = AddonCatalogEntry(
|
||||
{
|
||||
"freecad_min": Version(from_string="1.1"),
|
||||
"freecad_max": Version(from_string="1.3"),
|
||||
}
|
||||
)
|
||||
self.assertFalse(ac.is_compatible())
|
||||
|
||||
def test_version_match_with_min_and_max_bad_match_low(self):
|
||||
"""Given an AddonCatalogEntry with both a minimum and maximum FreeCAD version, a version
|
||||
lower than the minimum does not match."""
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=(1, 0, 3, "dev")):
|
||||
ac = AddonCatalogEntry(
|
||||
{
|
||||
"freecad_min": Version(from_string="1.1"),
|
||||
"freecad_max": Version(from_string="1.3"),
|
||||
}
|
||||
)
|
||||
self.assertFalse(ac.is_compatible())
|
||||
|
||||
|
||||
class TestAddonCatalog(unittest.TestCase):
|
||||
"""Tests for the AddonCatalog class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Start mock for addonmanager_licenses class."""
|
||||
global AddonCatalog
|
||||
global Version
|
||||
self.addon_patch = mock.patch.dict("sys.modules", {"addonmanager_licenses": mock.Mock()})
|
||||
self.mock_addon_module = self.addon_patch.start()
|
||||
from AddonCatalog import AddonCatalog
|
||||
from addonmanager_metadata import Version
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop patching the addonmanager_licenses class"""
|
||||
self.addon_patch.stop()
|
||||
|
||||
def test_single_addon_simple_entry(self):
|
||||
"""Test that an addon entry for an addon with only a git ref is accepted and added, and
|
||||
appears as an available addon."""
|
||||
data = {"AnAddon": [{"git_ref": "main"}]}
|
||||
catalog = AddonCatalog(data)
|
||||
ids = catalog.get_available_addon_ids()
|
||||
self.assertEqual(len(ids), 1)
|
||||
self.assertIn("AnAddon", ids)
|
||||
|
||||
def test_single_addon_max_single_entry(self):
|
||||
"""Test that an addon with the maximum possible data load is accepted."""
|
||||
data = {
|
||||
"AnAddon": [
|
||||
{
|
||||
"freecad_min": "0.21.0",
|
||||
"freecad_max": "1.99.99",
|
||||
"repository": "https://github.com/FreeCAD/FreeCAD",
|
||||
"git_ref": "main",
|
||||
"zip_url": "https://github.com/FreeCAD/FreeCAD/archive/main.zip",
|
||||
"note": "This is a fake repo, don't use it",
|
||||
"branch_display_name": "main",
|
||||
}
|
||||
]
|
||||
}
|
||||
catalog = AddonCatalog(data)
|
||||
ids = catalog.get_available_addon_ids()
|
||||
self.assertEqual(len(ids), 1)
|
||||
self.assertIn("AnAddon", ids)
|
||||
|
||||
def test_single_addon_multiple_entries(self):
|
||||
"""Test that an addon with multiple entries is accepted and only appears as a single
|
||||
addon."""
|
||||
data = {
|
||||
"AnAddon": [
|
||||
{
|
||||
"freecad_min": "1.0.0",
|
||||
"repository": "https://github.com/FreeCAD/FreeCAD",
|
||||
"git_ref": "main",
|
||||
},
|
||||
{
|
||||
"freecad_min": "0.21.0",
|
||||
"freecad_max": "0.21.99",
|
||||
"repository": "https://github.com/FreeCAD/FreeCAD",
|
||||
"git_ref": "0_21_compatibility_branch",
|
||||
"branch_display_name": "FreeCAD 0.21 Compatibility Branch",
|
||||
},
|
||||
]
|
||||
}
|
||||
catalog = AddonCatalog(data)
|
||||
ids = catalog.get_available_addon_ids()
|
||||
self.assertEqual(len(ids), 1)
|
||||
self.assertIn("AnAddon", ids)
|
||||
|
||||
def test_multiple_addon_entries(self):
|
||||
"""Test that multiple distinct addon entries are added as distinct addons"""
|
||||
data = {
|
||||
"AnAddon": [{"git_ref": "main"}],
|
||||
"AnotherAddon": [{"git_ref": "main"}],
|
||||
"YetAnotherAddon": [{"git_ref": "main"}],
|
||||
}
|
||||
catalog = AddonCatalog(data)
|
||||
ids = catalog.get_available_addon_ids()
|
||||
self.assertEqual(len(ids), 3)
|
||||
self.assertIn("AnAddon", ids)
|
||||
self.assertIn("AnotherAddon", ids)
|
||||
self.assertIn("YetAnotherAddon", ids)
|
||||
|
||||
def test_multiple_branches_single_match(self):
|
||||
"""Test that an addon with multiple branches representing different configurations of
|
||||
min and max FreeCAD versions returns only the appropriate match."""
|
||||
data = {
|
||||
"AnAddon": [
|
||||
{
|
||||
"freecad_min": "1.0.0",
|
||||
"repository": "https://github.com/FreeCAD/FreeCAD",
|
||||
"git_ref": "main",
|
||||
},
|
||||
{
|
||||
"freecad_min": "0.21.0",
|
||||
"freecad_max": "0.21.99",
|
||||
"repository": "https://github.com/FreeCAD/FreeCAD",
|
||||
"git_ref": "0_21_compatibility_branch",
|
||||
"branch_display_name": "FreeCAD 0.21 Compatibility Branch",
|
||||
},
|
||||
{
|
||||
"freecad_min": "0.19.0",
|
||||
"freecad_max": "0.20.99",
|
||||
"repository": "https://github.com/FreeCAD/FreeCAD",
|
||||
"git_ref": "0_19_compatibility_branch",
|
||||
"branch_display_name": "FreeCAD 0.19 Compatibility Branch",
|
||||
},
|
||||
]
|
||||
}
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=(1, 0, 3, "dev")):
|
||||
catalog = AddonCatalog(data)
|
||||
branches = catalog.get_available_branches("AnAddon")
|
||||
self.assertEqual(len(branches), 1)
|
||||
|
||||
def test_load_metadata_cache(self):
|
||||
"""Test that an addon with a known hash is correctly loaded (e.g. no exception is raised)"""
|
||||
data = {"AnAddon": [{"git_ref": "main"}]}
|
||||
catalog = AddonCatalog(data)
|
||||
sha = "cbce6737d7d058dca2b5ae3f2fdb8cc45b0c02bf711e75bdf5f12fb71ce87790"
|
||||
cache = {sha: "CacheData"}
|
||||
with patch("addonmanager_freecad_interface.Version", return_value=cache):
|
||||
with patch("Addon.Addon") as addon_mock:
|
||||
catalog.load_metadata_cache(cache)
|
||||
@@ -1,126 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import datetime
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import date
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.append("../..")
|
||||
|
||||
import addonmanager_cache as cache
|
||||
from AddonManagerTest.app.mocks import MockPref, MockExists
|
||||
|
||||
|
||||
class TestCache(TestCase):
|
||||
@patch("addonmanager_freecad_interface.getUserCachePath")
|
||||
@patch("addonmanager_freecad_interface.ParamGet")
|
||||
@patch("os.remove", MagicMock())
|
||||
@patch("os.makedirs", MagicMock())
|
||||
def test_local_cache_needs_update(self, param_mock: MagicMock, cache_mock: MagicMock):
|
||||
cache_mock.return_value = ""
|
||||
param_mock.return_value = MockPref()
|
||||
default_prefs = {
|
||||
"UpdateFrequencyComboEntry": 0,
|
||||
"LastCacheUpdate": "2000-01-01",
|
||||
"CustomRepoHash": "",
|
||||
"CustomRepositories": "",
|
||||
}
|
||||
today = date.today().isoformat()
|
||||
yesterday = (date.today() - datetime.timedelta(1)).isoformat()
|
||||
|
||||
# Organize these as subtests because of all the patching that has to be done: once we are in this function,
|
||||
# the patch is complete, and we can just modify the return values of the fakes one by one
|
||||
tests = (
|
||||
{
|
||||
"case": "No existing cache",
|
||||
"files_that_exist": [],
|
||||
"prefs_to_set": {},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Last cache update was interrupted",
|
||||
"files_that_exist": ["CACHE_UPDATE_INTERRUPTED"],
|
||||
"prefs_to_set": {},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Cache exists and updating is blocked",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {},
|
||||
"expect": False,
|
||||
},
|
||||
{
|
||||
"case": "Daily updates set and last update was long ago",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Daily updates set and last update was today",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1, "LastCacheUpdate": today},
|
||||
"expect": False,
|
||||
},
|
||||
{
|
||||
"case": "Daily updates set and last update was yesterday",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1, "LastCacheUpdate": yesterday},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Weekly updates set and last update was long ago",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Weekly updates set and last update was yesterday",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 2, "LastCacheUpdate": yesterday},
|
||||
"expect": False,
|
||||
},
|
||||
{
|
||||
"case": "Custom repo list changed",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"CustomRepositories": "NewRepo"},
|
||||
"expect": True,
|
||||
},
|
||||
)
|
||||
for test_case in tests:
|
||||
with self.subTest(test_case["case"]):
|
||||
case_prefs = default_prefs
|
||||
for pref, setting in test_case["prefs_to_set"].items():
|
||||
case_prefs[pref] = setting
|
||||
param_mock.return_value.set_prefs(case_prefs)
|
||||
exists_mock = MockExists(test_case["files_that_exist"])
|
||||
with patch("os.path.exists", exists_mock.exists):
|
||||
if test_case["expect"]:
|
||||
self.assertTrue(cache.local_cache_needs_update())
|
||||
else:
|
||||
self.assertFalse(cache.local_cache_needs_update())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,195 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from time import sleep
|
||||
import unittest
|
||||
|
||||
from addonmanager_dependency_installer import DependencyInstaller
|
||||
|
||||
|
||||
class CompleteProcessMock(subprocess.CompletedProcess):
|
||||
def __init__(self):
|
||||
super().__init__(["fake_arg"], 0)
|
||||
self.stdout = "Mock subprocess call stdout result"
|
||||
|
||||
|
||||
class SubprocessMock:
|
||||
def __init__(self):
|
||||
self.arg_log = []
|
||||
self.called = False
|
||||
self.call_count = 0
|
||||
self.delay = 0
|
||||
self.succeed = True
|
||||
|
||||
def subprocess_interceptor(self, args):
|
||||
self.arg_log.append(args)
|
||||
self.called = True
|
||||
self.call_count += 1
|
||||
sleep(self.delay)
|
||||
if self.succeed:
|
||||
return CompleteProcessMock()
|
||||
raise subprocess.CalledProcessError(1, " ".join(args), "Unit test mock output")
|
||||
|
||||
|
||||
class FakeFunction:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.call_count = 0
|
||||
self.return_value = None
|
||||
self.arg_log = []
|
||||
|
||||
def func_call(self, *args):
|
||||
self.arg_log.append(args)
|
||||
self.called = True
|
||||
self.call_count += 1
|
||||
return self.return_value
|
||||
|
||||
|
||||
class TestDependencyInstaller(unittest.TestCase):
|
||||
"""Test the dependency installation class"""
|
||||
|
||||
def setUp(self):
|
||||
self.subprocess_mock = SubprocessMock()
|
||||
self.test_object = DependencyInstaller([], ["required_py_package"], ["optional_py_package"])
|
||||
self.test_object._subprocess_wrapper = self.subprocess_mock.subprocess_interceptor
|
||||
self.signals_caught = []
|
||||
self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))
|
||||
self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))
|
||||
self.test_object.no_pip.connect(functools.partial(self.catch_signal, "no_pip"))
|
||||
self.test_object.no_python_exe.connect(
|
||||
functools.partial(self.catch_signal, "no_python_exe")
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def catch_signal(self, signal_name, *_):
|
||||
self.signals_caught.append(signal_name)
|
||||
|
||||
def test_run_no_pip(self):
|
||||
self.test_object._verify_pip = lambda: False
|
||||
self.test_object.run()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_run_with_pip(self):
|
||||
ff = FakeFunction()
|
||||
self.test_object._verify_pip = lambda: True
|
||||
self.test_object._install_python_packages = ff.func_call
|
||||
self.test_object.run()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
self.assertTrue(ff.called)
|
||||
|
||||
def test_run_with_no_packages(self):
|
||||
ff = FakeFunction()
|
||||
self.test_object._verify_pip = lambda: True
|
||||
self.test_object._install_python_packages = ff.func_call
|
||||
self.test_object.python_requires = []
|
||||
self.test_object.python_optional = []
|
||||
self.test_object.run()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
self.assertFalse(ff.called)
|
||||
|
||||
def test_install_python_packages_new_location(self):
|
||||
ff_required = FakeFunction()
|
||||
ff_optional = FakeFunction()
|
||||
self.test_object._install_required = ff_required.func_call
|
||||
self.test_object._install_optional = ff_optional.func_call
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.test_object.location = os.path.join(td, "UnitTestLocation")
|
||||
self.test_object._install_python_packages()
|
||||
self.assertTrue(ff_required.called)
|
||||
self.assertTrue(ff_optional.called)
|
||||
self.assertTrue(os.path.exists(self.test_object.location))
|
||||
|
||||
def test_install_python_packages_existing_location(self):
|
||||
ff_required = FakeFunction()
|
||||
ff_optional = FakeFunction()
|
||||
self.test_object._install_required = ff_required.func_call
|
||||
self.test_object._install_optional = ff_optional.func_call
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.test_object.location = td
|
||||
self.test_object._install_python_packages()
|
||||
self.assertTrue(ff_required.called)
|
||||
self.assertTrue(ff_optional.called)
|
||||
|
||||
def test_verify_pip_no_pip(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = False
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
result = self.test_object._verify_pip()
|
||||
self.assertFalse(result)
|
||||
self.assertIn("no_pip", self.signals_caught)
|
||||
|
||||
def test_verify_pip_with_pip(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = True
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
result = self.test_object._verify_pip()
|
||||
self.assertTrue(result)
|
||||
self.assertNotIn("no_pip", self.signals_caught)
|
||||
|
||||
def test_install_required_loops(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = True
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_requires = ["test1", "test2", "test3"]
|
||||
self.test_object._install_required("vendor_path")
|
||||
self.assertEqual(sm.call_count, 3)
|
||||
|
||||
def test_install_required_failure(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = False
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_requires = ["test1", "test2", "test3"]
|
||||
self.test_object._install_required("vendor_path")
|
||||
self.assertEqual(sm.call_count, 1)
|
||||
self.assertIn("failure", self.signals_caught)
|
||||
|
||||
def test_install_optional_loops(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = True
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_optional = ["test1", "test2", "test3"]
|
||||
self.test_object._install_optional("vendor_path")
|
||||
self.assertEqual(sm.call_count, 3)
|
||||
|
||||
def test_install_optional_failure(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = False
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_optional = ["test1", "test2", "test3"]
|
||||
self.test_object._install_optional("vendor_path")
|
||||
self.assertEqual(sm.call_count, 3)
|
||||
|
||||
def test_run_pip(self):
|
||||
pass
|
||||
@@ -1,299 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Tests for the Addon Manager's FreeCAD interface classes."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# pylint: disable=protected-access,import-outside-toplevel
|
||||
|
||||
|
||||
class TestConsole(unittest.TestCase):
|
||||
"""Tests for the Console"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.saved_freecad = None
|
||||
if "FreeCAD" in sys.modules:
|
||||
self.saved_freecad = sys.modules["FreeCAD"]
|
||||
sys.modules.pop("FreeCAD")
|
||||
if "addonmanager_freecad_interface" in sys.modules:
|
||||
sys.modules.pop("addonmanager_freecad_interface")
|
||||
sys.path.append("../../")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if "FreeCAD" in sys.modules:
|
||||
sys.modules.pop("FreeCAD")
|
||||
if self.saved_freecad is not None:
|
||||
sys.modules["FreeCAD"] = self.saved_freecad
|
||||
|
||||
def test_log_with_freecad(self):
|
||||
"""Ensure that if FreeCAD exists, the appropriate function is called"""
|
||||
sys.modules["FreeCAD"] = unittest.mock.MagicMock()
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
fc.Console.PrintLog("Test output")
|
||||
self.assertTrue(isinstance(fc.Console, unittest.mock.MagicMock))
|
||||
self.assertTrue(fc.Console.PrintLog.called)
|
||||
|
||||
def test_log_no_freecad(self):
|
||||
"""Test that if the FreeCAD import fails, the logger is set up correctly, and
|
||||
implements PrintLog"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
with patch("addonmanager_freecad_interface.logging", new=MagicMock()) as mock_logging:
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
fc.Console.PrintLog("Test output")
|
||||
self.assertTrue(isinstance(fc.Console, fc.ConsoleReplacement))
|
||||
self.assertTrue(mock_logging.log.called)
|
||||
|
||||
def test_message_no_freecad(self):
|
||||
"""Test that if the FreeCAD import fails the logger implements PrintMessage"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
with patch("addonmanager_freecad_interface.logging", new=MagicMock()) as mock_logging:
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
fc.Console.PrintMessage("Test output")
|
||||
self.assertTrue(mock_logging.info.called)
|
||||
|
||||
def test_warning_no_freecad(self):
|
||||
"""Test that if the FreeCAD import fails the logger implements PrintWarning"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
with patch("addonmanager_freecad_interface.logging", new=MagicMock()) as mock_logging:
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
fc.Console.PrintWarning("Test output")
|
||||
self.assertTrue(mock_logging.warning.called)
|
||||
|
||||
def test_error_no_freecad(self):
|
||||
"""Test that if the FreeCAD import fails the logger implements PrintError"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
with patch("addonmanager_freecad_interface.logging", new=MagicMock()) as mock_logging:
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
fc.Console.PrintError("Test output")
|
||||
self.assertTrue(mock_logging.error.called)
|
||||
|
||||
|
||||
class TestParameters(unittest.TestCase):
|
||||
"""Tests for the Parameters"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.saved_freecad = None
|
||||
if "FreeCAD" in sys.modules:
|
||||
self.saved_freecad = sys.modules["FreeCAD"]
|
||||
sys.modules.pop("FreeCAD")
|
||||
if "addonmanager_freecad_interface" in sys.modules:
|
||||
sys.modules.pop("addonmanager_freecad_interface")
|
||||
sys.path.append("../../")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if "FreeCAD" in sys.modules:
|
||||
sys.modules.pop("FreeCAD")
|
||||
if self.saved_freecad is not None:
|
||||
sys.modules["FreeCAD"] = self.saved_freecad
|
||||
|
||||
def test_param_get_with_freecad(self):
|
||||
"""Ensure that if FreeCAD exists, the built-in FreeCAD function is called"""
|
||||
sys.modules["FreeCAD"] = unittest.mock.MagicMock()
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
prefs = fc.ParamGet("some/fake/path")
|
||||
self.assertTrue(isinstance(prefs, unittest.mock.MagicMock))
|
||||
|
||||
def test_param_get_no_freecad(self):
|
||||
"""Test that if the FreeCAD import fails, param_get returns a ParametersReplacement"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
prefs = fc.ParamGet("some/fake/path")
|
||||
self.assertTrue(isinstance(prefs, fc.ParametersReplacement))
|
||||
|
||||
def test_replacement_getters_and_setters(self):
|
||||
"""Test that ParameterReplacement's getters, setters, and deleters work"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
prf = fc.ParamGet("some/fake/path")
|
||||
gs_types = [
|
||||
("Bool", prf.GetBool, prf.SetBool, prf.RemBool, True, False),
|
||||
("Int", prf.GetInt, prf.SetInt, prf.RemInt, 42, 0),
|
||||
("Float", prf.GetFloat, prf.SetFloat, prf.RemFloat, 1.2, 3.4),
|
||||
("String", prf.GetString, prf.SetString, prf.RemString, "test", "other"),
|
||||
]
|
||||
for gs_type in gs_types:
|
||||
with self.subTest(msg=f"Testing {gs_type[0]}", gs_type=gs_type):
|
||||
getter = gs_type[1]
|
||||
setter = gs_type[2]
|
||||
deleter = gs_type[3]
|
||||
value_1 = gs_type[4]
|
||||
value_2 = gs_type[5]
|
||||
self.assertEqual(getter("test", value_1), value_1)
|
||||
self.assertEqual(getter("test", value_2), value_2)
|
||||
self.assertNotIn("test", prf.parameters)
|
||||
setter("test", value_1)
|
||||
self.assertIn("test", prf.parameters)
|
||||
self.assertEqual(getter("test", value_2), value_1)
|
||||
deleter("test")
|
||||
self.assertNotIn("test", prf.parameters)
|
||||
|
||||
|
||||
class TestDataPaths(unittest.TestCase):
|
||||
"""Tests for the data paths"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.saved_freecad = None
|
||||
if "FreeCAD" in sys.modules:
|
||||
self.saved_freecad = sys.modules["FreeCAD"]
|
||||
sys.modules.pop("FreeCAD")
|
||||
if "addonmanager_freecad_interface" in sys.modules:
|
||||
sys.modules.pop("addonmanager_freecad_interface")
|
||||
sys.path.append("../../")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if "FreeCAD" in sys.modules:
|
||||
sys.modules.pop("FreeCAD")
|
||||
if self.saved_freecad is not None:
|
||||
sys.modules["FreeCAD"] = self.saved_freecad
|
||||
|
||||
def test_init_with_freecad(self):
|
||||
"""Ensure that if FreeCAD exists, the appropriate functions are called"""
|
||||
sys.modules["FreeCAD"] = unittest.mock.MagicMock()
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
data_paths = fc.DataPaths()
|
||||
self.assertTrue(sys.modules["FreeCAD"].getUserAppDataDir.called)
|
||||
self.assertTrue(sys.modules["FreeCAD"].getUserMacroDir.called)
|
||||
self.assertTrue(sys.modules["FreeCAD"].getUserCachePath.called)
|
||||
self.assertIsNotNone(data_paths.mod_dir)
|
||||
self.assertIsNotNone(data_paths.cache_dir)
|
||||
self.assertIsNotNone(data_paths.macro_dir)
|
||||
|
||||
def test_init_without_freecad(self):
|
||||
"""Ensure that if FreeCAD does not exist, the appropriate functions are called"""
|
||||
sys.modules["FreeCAD"] = None
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
data_paths = fc.DataPaths()
|
||||
self.assertIsNotNone(data_paths.mod_dir)
|
||||
self.assertIsNotNone(data_paths.cache_dir)
|
||||
self.assertIsNotNone(data_paths.macro_dir)
|
||||
self.assertNotEqual(data_paths.mod_dir, data_paths.cache_dir)
|
||||
self.assertNotEqual(data_paths.mod_dir, data_paths.macro_dir)
|
||||
self.assertNotEqual(data_paths.cache_dir, data_paths.macro_dir)
|
||||
|
||||
|
||||
class TestPreferences(unittest.TestCase):
|
||||
"""Tests for the preferences wrapper"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
sys.path.append("../../")
|
||||
import addonmanager_freecad_interface as fc
|
||||
|
||||
self.fc = fc
|
||||
|
||||
def tearDown(self) -> None:
|
||||
pass
|
||||
|
||||
def test_load_preferences_defaults(self):
|
||||
"""Preferences are loaded from a given file"""
|
||||
defaults = self.given_defaults()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
json_file = os.path.join(temp_dir, "defaults.json")
|
||||
with open(json_file, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(defaults))
|
||||
self.fc.Preferences._load_preferences_defaults(json_file)
|
||||
self.assertDictEqual(defaults, self.fc.Preferences.preferences_defaults)
|
||||
|
||||
def test_in_memory_defaults(self):
|
||||
"""Preferences are loaded from memory"""
|
||||
defaults = self.given_defaults()
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
self.assertDictEqual(defaults, prefs.preferences_defaults)
|
||||
|
||||
def test_get_good(self):
|
||||
"""Get returns results when matching an existing preference"""
|
||||
defaults = self.given_defaults()
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
self.assertEqual(prefs.get("TestBool"), defaults["TestBool"])
|
||||
self.assertEqual(prefs.get("TestInt"), defaults["TestInt"])
|
||||
self.assertEqual(prefs.get("TestFloat"), defaults["TestFloat"])
|
||||
self.assertEqual(prefs.get("TestString"), defaults["TestString"])
|
||||
|
||||
def test_get_nonexistent(self):
|
||||
"""Get raises an exception when asked for a non-existent preference"""
|
||||
defaults = self.given_defaults()
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
with self.assertRaises(RuntimeError):
|
||||
prefs.get("No_such_thing")
|
||||
|
||||
def test_get_bad_type(self):
|
||||
"""Get raises an exception when getting an unsupported type"""
|
||||
defaults = self.given_defaults()
|
||||
defaults["TestArray"] = ["This", "Is", "Legal", "JSON"]
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
with self.assertRaises(RuntimeError):
|
||||
prefs.get("TestArray")
|
||||
|
||||
def test_set_good(self):
|
||||
"""Set works when matching an existing preference"""
|
||||
defaults = self.given_defaults()
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
prefs.set("TestBool", False)
|
||||
self.assertEqual(prefs.get("TestBool"), False)
|
||||
prefs.set("TestInt", 4321)
|
||||
self.assertEqual(prefs.get("TestInt"), 4321)
|
||||
prefs.set("TestFloat", 3.14159)
|
||||
self.assertEqual(prefs.get("TestFloat"), 3.14159)
|
||||
prefs.set("TestString", "Forty two")
|
||||
self.assertEqual(prefs.get("TestString"), "Forty two")
|
||||
|
||||
def test_set_nonexistent(self):
|
||||
"""Set raises an exception when asked for a non-existent preference"""
|
||||
defaults = self.given_defaults()
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
with self.assertRaises(RuntimeError):
|
||||
prefs.get("No_such_thing")
|
||||
|
||||
def test_set_bad_type(self):
|
||||
"""Set raises an exception when setting an unsupported type"""
|
||||
defaults = self.given_defaults()
|
||||
defaults["TestArray"] = ["This", "Is", "Legal", "JSON"]
|
||||
prefs = self.fc.Preferences(defaults)
|
||||
with self.assertRaises(RuntimeError):
|
||||
prefs.get("TestArray")
|
||||
|
||||
@staticmethod
|
||||
def given_defaults():
|
||||
"""Get a dictionary of fake defaults for testing"""
|
||||
defaults = {
|
||||
"TestBool": True,
|
||||
"TestInt": 42,
|
||||
"TestFloat": 1.2,
|
||||
"TestString": "Test",
|
||||
}
|
||||
return defaults
|
||||
@@ -1,177 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
import FreeCAD
|
||||
|
||||
from addonmanager_git import GitManager, NoGitFound, GitFailed
|
||||
|
||||
try:
|
||||
git_manager = GitManager()
|
||||
except NoGitFound:
|
||||
git_manager = None
|
||||
|
||||
|
||||
@unittest.skipIf(git_manager is None, "No git executable -- not running git-based tests")
|
||||
class TestGit(unittest.TestCase):
|
||||
|
||||
MODULE = "test_git" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test case: called by the unit test system"""
|
||||
self.cwd = os.getcwd()
|
||||
test_data_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
git_repo_zip = os.path.join(test_data_dir, "test_repo.zip")
|
||||
self.test_dir = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "AddonManagerTests", "Git"
|
||||
)
|
||||
os.makedirs(self.test_dir, exist_ok=True)
|
||||
self.test_repo_remote = os.path.join(self.test_dir, "TEST_REPO_REMOTE")
|
||||
if os.path.exists(self.test_repo_remote):
|
||||
# Make sure any old copy that got left around is deleted
|
||||
self._rmdir(self.test_repo_remote)
|
||||
|
||||
if not os.path.exists(git_repo_zip):
|
||||
self.skipTest("Can't find test repo")
|
||||
return
|
||||
|
||||
with ZipFile(git_repo_zip, "r") as zip_repo:
|
||||
zip_repo.extractall(self.test_repo_remote)
|
||||
self.test_repo_remote = os.path.join(self.test_repo_remote, "test_repo")
|
||||
|
||||
self.git = git_manager
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after the test"""
|
||||
os.chdir(self.cwd)
|
||||
# self._rmdir(self.test_dir)
|
||||
os.rename(self.test_dir, self.test_dir + ".old." + str(time.time()))
|
||||
|
||||
def test_clone(self):
|
||||
"""Test git clone"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
self.assertTrue(os.path.exists(checkout_dir))
|
||||
self.assertTrue(os.path.exists(os.path.join(checkout_dir, ".git")))
|
||||
self.assertEqual(os.getcwd(), self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_checkout(self):
|
||||
"""Test git checkout"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
|
||||
self.git.checkout(checkout_dir, "HEAD~1")
|
||||
status = self.git.status(checkout_dir).strip()
|
||||
expected_status = "## HEAD (no branch)"
|
||||
self.assertEqual(status, expected_status)
|
||||
|
||||
self.assertEqual(os.getcwd(), self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_update(self):
|
||||
"""Test using git to update the local repo"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
|
||||
self.git.reset(checkout_dir, ["--hard", "HEAD~1"])
|
||||
self.assertTrue(self.git.update_available(checkout_dir))
|
||||
self.git.update(checkout_dir)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
self.assertEqual(os.getcwd(), self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_tag_and_branch(self):
|
||||
"""Test checking the currently checked-out tag"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
|
||||
expected_tag = "TestTag"
|
||||
self.git.checkout(checkout_dir, expected_tag)
|
||||
found_tag = self.git.current_tag(checkout_dir)
|
||||
self.assertEqual(found_tag, expected_tag)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
|
||||
expected_branch = "TestBranch"
|
||||
self.git.checkout(checkout_dir, expected_branch)
|
||||
found_branch = self.git.current_branch(checkout_dir)
|
||||
self.assertEqual(found_branch, expected_branch)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
|
||||
expected_branch = "main"
|
||||
self.git.checkout(checkout_dir, expected_branch)
|
||||
found_branch = self.git.current_branch(checkout_dir)
|
||||
self.assertEqual(found_branch, expected_branch)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
|
||||
self.assertEqual(os.getcwd(), self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_get_remote(self):
|
||||
"""Test getting the remote location"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
expected_remote = self.test_repo_remote
|
||||
returned_remote = self.git.get_remote(checkout_dir)
|
||||
self.assertEqual(expected_remote, returned_remote)
|
||||
self.assertEqual(os.getcwd(), self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_repair(self):
|
||||
"""Test the repair feature (and some exception throwing)"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
remote = self.git.get_remote(checkout_dir)
|
||||
git_dir = os.path.join(checkout_dir, ".git")
|
||||
self.assertTrue(os.path.exists(git_dir))
|
||||
self._rmdir(git_dir)
|
||||
|
||||
# Make sure that we've truly broken the install
|
||||
with self.assertRaises(GitFailed):
|
||||
self.git.status(checkout_dir)
|
||||
|
||||
self.git.repair(remote, checkout_dir)
|
||||
status = self.git.status(checkout_dir)
|
||||
self.assertEqual(status, "## main...origin/main\n")
|
||||
self.assertEqual(os.getcwd(), self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def _rmdir(self, path):
|
||||
try:
|
||||
shutil.rmtree(path, onerror=self._remove_readonly)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def _remove_readonly(self, func, path, _) -> None:
|
||||
"""Remove a read-only file."""
|
||||
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
def _clone_test_repo(self) -> str:
|
||||
checkout_dir = os.path.join(self.test_dir, "test_repo")
|
||||
try:
|
||||
# Git won't clone to an existing directory, so make sure to remove it first
|
||||
if os.path.exists(checkout_dir):
|
||||
self._rmdir(checkout_dir)
|
||||
self.git.clone(self.test_repo_remote, checkout_dir)
|
||||
except GitFailed as e:
|
||||
self.fail(str(e))
|
||||
return checkout_dir
|
||||
@@ -1,412 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Contains the unit test class for addonmanager_installer.py non-GUI functionality."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from zipfile import ZipFile
|
||||
import sys
|
||||
|
||||
sys.path.append("../../") # So the IDE can find the imports below
|
||||
|
||||
import FreeCAD
|
||||
from addonmanager_installer import InstallationMethod, AddonInstaller, MacroInstaller
|
||||
from addonmanager_git import GitManager, initialize_git
|
||||
from addonmanager_metadata import MetadataReader
|
||||
from Addon import Addon
|
||||
from AddonManagerTest.app.mocks import MockAddon, MockMacro
|
||||
|
||||
|
||||
class TestAddonInstaller(unittest.TestCase):
|
||||
"""Test class for addonmanager_installer.py non-GUI functionality"""
|
||||
|
||||
MODULE = "test_installer" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize data needed for all tests"""
|
||||
# self.start_time = time.perf_counter()
|
||||
self.test_data_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
self.real_addon = Addon(
|
||||
"TestAddon",
|
||||
"https://github.com/FreeCAD/FreeCAD-addons",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
self.mock_addon = MockAddon()
|
||||
|
||||
def tearDown(self):
|
||||
"""Finalize the test."""
|
||||
# end_time = time.perf_counter()
|
||||
# print(f"Test '{self.id()}' ran in {end_time-self.start_time:.4f} seconds")
|
||||
|
||||
def test_validate_object(self):
|
||||
"""An object is valid if it has a name, url, and branch attribute."""
|
||||
|
||||
AddonInstaller._validate_object(self.real_addon) # Won't raise
|
||||
AddonInstaller._validate_object(self.mock_addon) # Won't raise
|
||||
|
||||
class NoName:
|
||||
def __init__(self):
|
||||
self.url = "https://github.com/FreeCAD/FreeCAD-addons"
|
||||
self.branch = "master"
|
||||
|
||||
no_name = NoName()
|
||||
with self.assertRaises(RuntimeError):
|
||||
AddonInstaller._validate_object(no_name)
|
||||
|
||||
class NoUrl:
|
||||
def __init__(self):
|
||||
self.name = "TestAddon"
|
||||
self.branch = "master"
|
||||
|
||||
no_url = NoUrl()
|
||||
with self.assertRaises(RuntimeError):
|
||||
AddonInstaller._validate_object(no_url)
|
||||
|
||||
class NoBranch:
|
||||
def __init__(self):
|
||||
self.name = "TestAddon"
|
||||
self.url = "https://github.com/FreeCAD/FreeCAD-addons"
|
||||
|
||||
no_branch = NoBranch()
|
||||
with self.assertRaises(RuntimeError):
|
||||
AddonInstaller._validate_object(no_branch)
|
||||
|
||||
def test_update_metadata(self):
|
||||
"""If a metadata file exists in the installation location, it should be
|
||||
loaded."""
|
||||
addon = Mock()
|
||||
addon.name = "MockAddon"
|
||||
installer = AddonInstaller(addon, [])
|
||||
installer._update_metadata() # Does nothing, but should not crash
|
||||
|
||||
installer = AddonInstaller(self.real_addon, [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer.installation_path = temp_dir
|
||||
installer._update_metadata()
|
||||
addon_dir = os.path.join(temp_dir, self.real_addon.name)
|
||||
os.mkdir(addon_dir)
|
||||
shutil.copy(
|
||||
os.path.join(self.test_data_dir, "good_package.xml"),
|
||||
os.path.join(addon_dir, "package.xml"),
|
||||
)
|
||||
good_metadata = MetadataReader.from_file(os.path.join(addon_dir, "package.xml"))
|
||||
installer._update_metadata()
|
||||
self.assertEqual(self.real_addon.installed_version, good_metadata.version)
|
||||
|
||||
def test_finalize_zip_installation_non_github(self):
|
||||
"""Ensure that zip files are correctly extracted."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_simple_repo = os.path.join(self.test_data_dir, "test_simple_repo.zip")
|
||||
non_gh_mock = MockAddon()
|
||||
non_gh_mock.url = test_simple_repo[:-4]
|
||||
non_gh_mock.name = "NonGitHubMock"
|
||||
installer = AddonInstaller(non_gh_mock, [])
|
||||
installer.installation_path = temp_dir
|
||||
installer._finalize_zip_installation(test_simple_repo)
|
||||
expected_location = os.path.join(temp_dir, non_gh_mock.name, "README")
|
||||
self.assertTrue(os.path.isfile(expected_location), "Non-GitHub zip extraction failed")
|
||||
|
||||
def test_finalize_zip_installation_github(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_github_style_repo = os.path.join(self.test_data_dir, "test_github_style_repo.zip")
|
||||
self.mock_addon.url = "https://github.com/something/test_github_style_repo"
|
||||
self.mock_addon.branch = "master"
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
installer.installation_path = temp_dir
|
||||
installer._finalize_zip_installation(test_github_style_repo)
|
||||
expected_location = os.path.join(temp_dir, self.mock_addon.name, "README")
|
||||
self.assertTrue(os.path.isfile(expected_location), "GitHub zip extraction failed")
|
||||
|
||||
def test_code_in_branch_subdirectory_true(self):
|
||||
"""When there is a subdirectory with the branch name in it, find it"""
|
||||
self.mock_addon.url = "https://something.com/something_else/something"
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.mkdir(os.path.join(temp_dir, f"something-{self.mock_addon.branch}"))
|
||||
result = installer._code_in_branch_subdirectory(temp_dir)
|
||||
self.assertTrue(result, "Failed to find ZIP subdirectory")
|
||||
|
||||
def test_code_in_branch_subdirectory_false(self):
|
||||
"""When there is not a subdirectory with the branch name in it, don't find
|
||||
one"""
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = installer._code_in_branch_subdirectory(temp_dir)
|
||||
self.assertFalse(result, "Found ZIP subdirectory when there was none")
|
||||
|
||||
def test_code_in_branch_subdirectory_more_than_one(self):
|
||||
"""When there are multiple subdirectories, never find a branch subdirectory"""
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.mkdir(os.path.join(temp_dir, f"{self.mock_addon.name}-{self.mock_addon.branch}"))
|
||||
os.mkdir(os.path.join(temp_dir, "AnotherSubdir"))
|
||||
result = installer._code_in_branch_subdirectory(temp_dir)
|
||||
self.assertFalse(result, "Found ZIP subdirectory when there were multiple subdirs")
|
||||
|
||||
def test_move_code_out_of_subdirectory(self):
|
||||
"""All files are moved out and the subdirectory is deleted"""
|
||||
self.mock_addon.url = "https://something.com/something_else/something"
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
subdir = os.path.join(temp_dir, f"something-{self.mock_addon.branch}")
|
||||
os.mkdir(subdir)
|
||||
with open(os.path.join(subdir, "README.txt"), "w", encoding="utf-8") as f:
|
||||
f.write("# Test file for unit testing")
|
||||
with open(os.path.join(subdir, "AnotherFile.txt"), "w", encoding="utf-8") as f:
|
||||
f.write("# Test file for unit testing")
|
||||
installer._move_code_out_of_subdirectory(temp_dir)
|
||||
self.assertTrue(os.path.isfile(os.path.join(temp_dir, "README.txt")))
|
||||
self.assertTrue(os.path.isfile(os.path.join(temp_dir, "AnotherFile.txt")))
|
||||
self.assertFalse(os.path.isdir(subdir))
|
||||
|
||||
def test_install_by_git(self):
|
||||
"""Test using git to install. Depends on there being a local git
|
||||
installation: the test is skipped if there is no local git."""
|
||||
git_manager = initialize_git()
|
||||
if not git_manager:
|
||||
self.skipTest("git not found, skipping git installer tests")
|
||||
return
|
||||
|
||||
# Our test git repo has to be in a zipfile, otherwise it cannot itself be
|
||||
# stored in git, since it has a .git subdirectory.
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
git_repo_zip = os.path.join(self.test_data_dir, "test_repo.zip")
|
||||
with ZipFile(git_repo_zip, "r") as zip_repo:
|
||||
zip_repo.extractall(temp_dir)
|
||||
|
||||
mock_addon = MockAddon()
|
||||
mock_addon.url = os.path.join(temp_dir, "test_repo")
|
||||
mock_addon.branch = "main"
|
||||
installer = AddonInstaller(mock_addon, [])
|
||||
installer.installation_path = os.path.join(temp_dir, "installed_addon")
|
||||
installer._install_by_git()
|
||||
|
||||
self.assertTrue(os.path.exists(installer.installation_path))
|
||||
addon_name_dir = os.path.join(installer.installation_path, mock_addon.name)
|
||||
self.assertTrue(os.path.exists(addon_name_dir))
|
||||
readme = os.path.join(addon_name_dir, "README.md")
|
||||
self.assertTrue(os.path.exists(readme))
|
||||
|
||||
def test_install_by_copy(self):
|
||||
"""Test using a simple filesystem copy to install an addon."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
git_repo_zip = os.path.join(self.test_data_dir, "test_repo.zip")
|
||||
with ZipFile(git_repo_zip, "r") as zip_repo:
|
||||
zip_repo.extractall(temp_dir)
|
||||
|
||||
mock_addon = MockAddon()
|
||||
mock_addon.url = os.path.join(temp_dir, "test_repo")
|
||||
mock_addon.branch = "main"
|
||||
installer = AddonInstaller(mock_addon, [])
|
||||
installer.addon_to_install = mock_addon
|
||||
installer.installation_path = os.path.join(temp_dir, "installed_addon")
|
||||
installer._install_by_copy()
|
||||
|
||||
self.assertTrue(os.path.exists(installer.installation_path))
|
||||
addon_name_dir = os.path.join(installer.installation_path, mock_addon.name)
|
||||
self.assertTrue(os.path.exists(addon_name_dir))
|
||||
readme = os.path.join(addon_name_dir, "README.md")
|
||||
self.assertTrue(os.path.exists(readme))
|
||||
|
||||
def test_determine_install_method_local_path(self):
|
||||
"""Test which install methods are accepted for a local path"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.COPY)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
git_manager = initialize_git()
|
||||
if git_manager:
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.GIT)
|
||||
self.assertEqual(method, InstallationMethod.GIT)
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.ZIP)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.ANY)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
|
||||
def test_determine_install_method_file_url(self):
|
||||
"""Test which install methods are accepted for a file:// url"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
temp_dir = "file://" + temp_dir.replace(os.path.sep, "/")
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.COPY)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
git_manager = initialize_git()
|
||||
if git_manager:
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.GIT)
|
||||
self.assertEqual(method, InstallationMethod.GIT)
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.ZIP)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_dir, InstallationMethod.ANY)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
|
||||
def test_determine_install_method_local_zip(self):
|
||||
"""Test which install methods are accepted for a local path to a zipfile"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
temp_file = os.path.join(temp_dir, "dummy.zip")
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.COPY)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.GIT)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ZIP)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ANY)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
|
||||
def test_determine_install_method_remote_zip(self):
|
||||
"""Test which install methods are accepted for a remote path to a zipfile"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
|
||||
temp_file = "https://freecad.org/dummy.zip" # Doesn't have to actually exist!
|
||||
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.COPY)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.GIT)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ZIP)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ANY)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
|
||||
def test_determine_install_method_https_known_sites_copy(self):
|
||||
"""Test which install methods are accepted for an https GitHub URL"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
installer.git_manager = True
|
||||
|
||||
for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]:
|
||||
with self.subTest(site=site):
|
||||
temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist!
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.COPY)
|
||||
self.assertIsNone(method, f"Allowed copying from {site} URL")
|
||||
|
||||
def test_determine_install_method_https_known_sites_git(self):
|
||||
"""Test which install methods are accepted for an https GitHub URL"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
installer.git_manager = True
|
||||
|
||||
for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]:
|
||||
with self.subTest(site=site):
|
||||
temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist!
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.GIT)
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.GIT,
|
||||
f"Failed to allow git access to {site} URL",
|
||||
)
|
||||
|
||||
def test_determine_install_method_https_known_sites_zip(self):
|
||||
"""Test which install methods are accepted for an https GitHub URL"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
installer.git_manager = True
|
||||
|
||||
for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]:
|
||||
with self.subTest(site=site):
|
||||
temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist!
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ZIP)
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.ZIP,
|
||||
f"Failed to allow zip access to {site} URL",
|
||||
)
|
||||
|
||||
def test_determine_install_method_https_known_sites_any_gm(self):
|
||||
"""Test which install methods are accepted for an https GitHub URL"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
installer.git_manager = True
|
||||
|
||||
for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]:
|
||||
with self.subTest(site=site):
|
||||
temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist!
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ANY)
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.GIT,
|
||||
f"Failed to allow git access to {site} URL",
|
||||
)
|
||||
|
||||
def test_determine_install_method_https_known_sites_any_no_gm(self):
|
||||
"""Test which install methods are accepted for an https GitHub URL"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [])
|
||||
installer.git_manager = None
|
||||
|
||||
for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]:
|
||||
with self.subTest(site=site):
|
||||
temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist!
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ANY)
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.ZIP,
|
||||
f"Failed to allow zip access to {site} URL",
|
||||
)
|
||||
|
||||
def test_fcmacro_copying(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_addon = MockAddon()
|
||||
mock_addon.url = os.path.join(self.test_data_dir, "test_addon_with_fcmacro.zip")
|
||||
installer = AddonInstaller(mock_addon, [])
|
||||
installer.installation_path = temp_dir
|
||||
installer.macro_installation_path = os.path.join(temp_dir, "Macros")
|
||||
installer.run()
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, "Macros", "TestMacro.FCMacro")),
|
||||
"FCMacro file was not copied to macro installation location",
|
||||
)
|
||||
|
||||
|
||||
class TestMacroInstaller(unittest.TestCase):
|
||||
MODULE = "test_installer" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the mock objects"""
|
||||
|
||||
self.mock = MockAddon()
|
||||
self.mock.macro = MockMacro()
|
||||
|
||||
def test_installation(self):
|
||||
"""Test the wrapper around the macro installer"""
|
||||
|
||||
# Note that this doesn't test the underlying Macro object's install function,
|
||||
# it only tests whether that function is called appropriately by the
|
||||
# MacroInstaller wrapper.
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = MacroInstaller(self.mock)
|
||||
installer.installation_path = temp_dir
|
||||
installation_succeeded = installer.run()
|
||||
self.assertTrue(installation_succeeded)
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock.macro.filename)))
|
||||
@@ -1,213 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Dict
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.path.append("../../") # So the IDE can find the
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from addonmanager_macro import Macro
|
||||
|
||||
|
||||
class TestMacro(unittest.TestCase):
|
||||
MODULE = "test_macro" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
def test_basic_metadata(self):
|
||||
replacements = {
|
||||
"COMMENT": "test comment",
|
||||
"WEB": "https://test.url",
|
||||
"VERSION": "1.2.3",
|
||||
"AUTHOR": "Test Author",
|
||||
"DATE": "2022-03-09",
|
||||
"ICON": "testicon.svg",
|
||||
}
|
||||
m = self.generate_macro(replacements)
|
||||
self.assertEqual(m.comment, replacements["COMMENT"])
|
||||
self.assertEqual(m.url, replacements["WEB"])
|
||||
self.assertEqual(m.version, replacements["VERSION"])
|
||||
self.assertEqual(m.author, replacements["AUTHOR"])
|
||||
self.assertEqual(m.date, replacements["DATE"])
|
||||
self.assertEqual(m.icon, replacements["ICON"])
|
||||
|
||||
def test_other_files(self):
|
||||
replacements = {
|
||||
"FILES": "file_a,file_b,file_c",
|
||||
}
|
||||
m = self.generate_macro(replacements)
|
||||
self.assertEqual(len(m.other_files), 3)
|
||||
self.assertEqual(m.other_files[0], "file_a")
|
||||
self.assertEqual(m.other_files[1], "file_b")
|
||||
self.assertEqual(m.other_files[2], "file_c")
|
||||
|
||||
replacements = {
|
||||
"FILES": "file_a, file_b, file_c",
|
||||
}
|
||||
m = self.generate_macro(replacements)
|
||||
self.assertEqual(len(m.other_files), 3)
|
||||
self.assertEqual(m.other_files[0], "file_a")
|
||||
self.assertEqual(m.other_files[1], "file_b")
|
||||
self.assertEqual(m.other_files[2], "file_c")
|
||||
|
||||
replacements = {
|
||||
"FILES": "file_a file_b file_c",
|
||||
}
|
||||
m = self.generate_macro(replacements)
|
||||
self.assertEqual(len(m.other_files), 1)
|
||||
self.assertEqual(m.other_files[0], "file_a file_b file_c")
|
||||
|
||||
def test_version_from_string(self):
|
||||
replacements = {
|
||||
"VERSION": "1.2.3",
|
||||
}
|
||||
m = self.generate_macro(replacements)
|
||||
self.assertEqual(m.version, "1.2.3")
|
||||
|
||||
def test_version_from_date(self):
|
||||
replacements = {
|
||||
"DATE": "2022-03-09",
|
||||
}
|
||||
outfile = self.generate_macro_file(replacements)
|
||||
with open(outfile) as f:
|
||||
lines = f.readlines()
|
||||
output_lines = []
|
||||
for line in lines:
|
||||
if "VERSION" in line:
|
||||
line = "__Version__ = __Date__"
|
||||
output_lines.append(line)
|
||||
with open(outfile, "w") as f:
|
||||
f.write("\n".join(output_lines))
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
self.assertEqual(m.version, "2022-03-09")
|
||||
|
||||
def test_version_from_float(self):
|
||||
outfile = self.generate_macro_file()
|
||||
with open(outfile) as f:
|
||||
lines = f.readlines()
|
||||
output_lines = []
|
||||
for line in lines:
|
||||
if "VERSION" in line:
|
||||
line = "__Version__ = 1.23"
|
||||
output_lines.append(line)
|
||||
with open(outfile, "w") as f:
|
||||
f.write("\n".join(output_lines))
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
self.assertEqual(m.version, "1.23")
|
||||
|
||||
def test_version_from_int(self):
|
||||
outfile = self.generate_macro_file()
|
||||
with open(outfile) as f:
|
||||
lines = f.readlines()
|
||||
output_lines = []
|
||||
for line in lines:
|
||||
if "VERSION" in line:
|
||||
line = "__Version__ = 1"
|
||||
output_lines.append(line)
|
||||
with open(outfile, "w") as f:
|
||||
f.write("\n".join(output_lines))
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
self.assertEqual(m.version, "1")
|
||||
|
||||
def test_xpm(self):
|
||||
outfile = self.generate_macro_file()
|
||||
xpm_data = """/* XPM */
|
||||
static char * blarg_xpm[] = {
|
||||
"16 7 2 1",
|
||||
"* c #000000",
|
||||
". c #ffffff",
|
||||
"**..*...........",
|
||||
"*.*.*...........",
|
||||
"**..*..**.**..**",
|
||||
"*.*.*.*.*.*..*.*",
|
||||
"**..*..**.*...**",
|
||||
"...............*",
|
||||
".............**."
|
||||
};"""
|
||||
with open(outfile) as f:
|
||||
contents = f.read()
|
||||
contents += f'\n__xpm__ = """{xpm_data}"""\n'
|
||||
|
||||
with open(outfile, "w") as f:
|
||||
f.write(contents)
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
self.assertEqual(m.xpm, xpm_data)
|
||||
|
||||
def generate_macro_file(self, replacements: Dict[str, str] = {}) -> os.PathLike:
|
||||
with open(os.path.join(self.test_dir, "macro_template.FCStd")) as f:
|
||||
lines = f.readlines()
|
||||
outfile = tempfile.NamedTemporaryFile(mode="wt", delete=False)
|
||||
for line in lines:
|
||||
for key, value in replacements.items():
|
||||
line = line.replace(key, value)
|
||||
|
||||
outfile.write(line)
|
||||
outfile.close()
|
||||
return outfile.name
|
||||
|
||||
def generate_macro(self, replacements: Dict[str, str] = {}) -> Macro:
|
||||
outfile = self.generate_macro_file(replacements)
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
os.unlink(outfile)
|
||||
return m
|
||||
|
||||
def test_fetch_raw_code_no_data(self):
|
||||
m = Macro("Unit Test Macro")
|
||||
Macro.blocking_get = MagicMock(return_value=None)
|
||||
returned_data = m._fetch_raw_code(
|
||||
'rawcodeurl <a href="https://fake_url.com">Totally fake</a>'
|
||||
)
|
||||
self.assertIsNone(returned_data)
|
||||
m.blocking_get.assert_called_with("https://fake_url.com")
|
||||
Macro.blocking_get = None
|
||||
|
||||
def test_fetch_raw_code_no_url(self):
|
||||
m = Macro("Unit Test Macro")
|
||||
Macro.blocking_get = MagicMock(return_value=None)
|
||||
returned_data = m._fetch_raw_code("Fake pagedata with no URL at all.")
|
||||
self.assertIsNone(returned_data)
|
||||
m.blocking_get.assert_not_called()
|
||||
Macro.blocking_get = None
|
||||
|
||||
def test_fetch_raw_code_with_data(self):
|
||||
m = Macro("Unit Test Macro")
|
||||
Macro.blocking_get = MagicMock(return_value=b"Data returned to _fetch_raw_code")
|
||||
returned_data = m._fetch_raw_code(
|
||||
'rawcodeurl <a href="https://fake_url.com">Totally fake</a>'
|
||||
)
|
||||
self.assertEqual(returned_data, "Data returned to _fetch_raw_code")
|
||||
Macro.blocking_get = None
|
||||
@@ -1,344 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Tests for the MacroParser class"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.append("../../") # So the IDE can find the classes to run with
|
||||
|
||||
from addonmanager_macro_parser import MacroParser
|
||||
from AddonManagerTest.app.mocks import MockConsole, CallCatcher, MockThread
|
||||
|
||||
|
||||
# pylint: disable=protected-access, too-many-public-methods
|
||||
|
||||
|
||||
class TestMacroParser(unittest.TestCase):
|
||||
"""Test the MacroParser class"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.test_object = MacroParser("UnitTestMacro")
|
||||
self.test_object.console = MockConsole()
|
||||
self.test_object.current_thread = MockThread()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
pass
|
||||
|
||||
def test_fill_details_from_code_normal(self):
|
||||
"""Test to make sure _process_line gets called as expected"""
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_line = catcher.catch_call
|
||||
fake_macro_data = self.given_some_lines(20, 10)
|
||||
self.test_object.fill_details_from_code(fake_macro_data)
|
||||
self.assertEqual(catcher.call_count, 10)
|
||||
|
||||
def test_fill_details_from_code_too_many_lines(self):
|
||||
"""Test to make sure _process_line gets limited as expected"""
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_line = catcher.catch_call
|
||||
self.test_object.MAX_LINES_TO_SEARCH = 5
|
||||
fake_macro_data = self.given_some_lines(20, 10)
|
||||
self.test_object.fill_details_from_code(fake_macro_data)
|
||||
self.assertEqual(catcher.call_count, 5)
|
||||
|
||||
def test_fill_details_from_code_thread_interrupted(self):
|
||||
"""Test to make sure _process_line gets stopped as expected"""
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_line = catcher.catch_call
|
||||
self.test_object.current_thread.interrupt_after_n_calls = 6 # Stop on the 6th
|
||||
fake_macro_data = self.given_some_lines(20, 10)
|
||||
self.test_object.fill_details_from_code(fake_macro_data)
|
||||
self.assertEqual(catcher.call_count, 5)
|
||||
|
||||
@staticmethod
|
||||
def given_some_lines(num_lines, num_dunder_lines) -> str:
|
||||
"""Generate fake macro header data with the given number of lines and number of
|
||||
lines beginning with a double-underscore."""
|
||||
result = ""
|
||||
for i in range(num_lines):
|
||||
if i < num_dunder_lines:
|
||||
result += f"__something_{i}__ = 'Test{i}' # A line to be scanned\n"
|
||||
else:
|
||||
result += f"# Nothing to see on line {i}\n"
|
||||
return result
|
||||
|
||||
def test_process_line_known_lines(self):
|
||||
"""Lines starting with keys are processed"""
|
||||
test_lines = ["__known_key__ = 'Test'", "__another_known_key__ = 'Test'"]
|
||||
for line in test_lines:
|
||||
with self.subTest(line=line):
|
||||
self.test_object.remaining_item_map = {
|
||||
"__known_key__": "known_key",
|
||||
"__another_known_key__": "another_known_key",
|
||||
}
|
||||
content_lines = io.StringIO(line)
|
||||
read_in_line = content_lines.readline()
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_key = catcher.catch_call
|
||||
self.test_object._process_line(read_in_line, content_lines)
|
||||
self.assertTrue(catcher.called, "_process_key was not called for a known key")
|
||||
|
||||
def test_process_line_unknown_lines(self):
|
||||
"""Lines starting with non-keys are not processed"""
|
||||
test_lines = [
|
||||
"# Just a line with a comment",
|
||||
"\n",
|
||||
"__dont_know_this_one__ = 'Who cares?'",
|
||||
"# __known_key__ = 'Aha, but it is commented out!'",
|
||||
]
|
||||
for line in test_lines:
|
||||
with self.subTest(line=line):
|
||||
self.test_object.remaining_item_map = {
|
||||
"__known_key__": "known_key",
|
||||
"__another_known_key__": "another_known_key",
|
||||
}
|
||||
content_lines = io.StringIO(line)
|
||||
read_in_line = content_lines.readline()
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_key = catcher.catch_call
|
||||
self.test_object._process_line(read_in_line, content_lines)
|
||||
self.assertFalse(catcher.called, "_process_key was called for an unknown key")
|
||||
|
||||
def test_process_key_standard(self):
|
||||
"""Normal expected data is processed"""
|
||||
self.test_object._reset_map()
|
||||
in_memory_data = '__comment__ = "Test"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
self.test_object._process_key("__comment__", line, content_lines)
|
||||
self.assertTrue(self.test_object.parse_results["comment"], "Test")
|
||||
|
||||
def test_process_key_special(self):
|
||||
"""Special handling for version = date is processed"""
|
||||
self.test_object._reset_map()
|
||||
self.test_object.parse_results["date"] = "2001-01-01"
|
||||
in_memory_data = "__version__ = __date__"
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
self.test_object._process_key("__version__", line, content_lines)
|
||||
self.assertTrue(self.test_object.parse_results["version"], "2001-01-01")
|
||||
|
||||
def test_handle_backslash_continuation_no_backslashes(self):
|
||||
"""The backslash handling code doesn't change a line with no backslashes"""
|
||||
in_memory_data = '"Not a backslash in sight"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result = self.test_object._handle_backslash_continuation(line, content_lines)
|
||||
self.assertEqual(result, in_memory_data)
|
||||
|
||||
def test_handle_backslash_continuation(self):
|
||||
"""Lines ending in a backslash get stripped and concatenated"""
|
||||
in_memory_data = '"Line1\\\nLine2\\\nLine3\\\nLine4"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result = self.test_object._handle_backslash_continuation(line, content_lines)
|
||||
self.assertEqual(result, '"Line1Line2Line3Line4"')
|
||||
|
||||
def test_handle_triple_quoted_string_no_triple_quotes(self):
|
||||
"""The triple-quote handler leaves alone lines without triple-quotes"""
|
||||
in_memory_data = '"Line1"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result, was_triple_quoted = self.test_object._handle_triple_quoted_string(
|
||||
line, content_lines
|
||||
)
|
||||
self.assertEqual(result, in_memory_data)
|
||||
self.assertFalse(was_triple_quoted)
|
||||
|
||||
def test_handle_triple_quoted_string(self):
|
||||
"""Data is extracted across multiple lines for a triple-quoted string"""
|
||||
in_memory_data = '"""Line1\nLine2\nLine3\nLine4"""\nLine5\n'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result, was_triple_quoted = self.test_object._handle_triple_quoted_string(
|
||||
line, content_lines
|
||||
)
|
||||
self.assertEqual(result, '"""Line1\nLine2\nLine3\nLine4"""')
|
||||
self.assertTrue(was_triple_quoted)
|
||||
|
||||
def test_strip_quotes_single(self):
|
||||
"""Single quotes are stripped from the final string"""
|
||||
expected = "test"
|
||||
quoted = f"'{expected}'"
|
||||
actual = self.test_object._strip_quotes(quoted)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strip_quotes_double(self):
|
||||
"""Double quotes are stripped from the final string"""
|
||||
expected = "test"
|
||||
quoted = f'"{expected}"'
|
||||
actual = self.test_object._strip_quotes(quoted)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strip_quotes_triple(self):
|
||||
"""Triple quotes are stripped from the final string"""
|
||||
expected = "test"
|
||||
quoted = f'"""{expected}"""'
|
||||
actual = self.test_object._strip_quotes(quoted)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strip_quotes_unquoted(self):
|
||||
"""Unquoted data results in None"""
|
||||
unquoted = "This has no quotation marks of any kind"
|
||||
actual = self.test_object._strip_quotes(unquoted)
|
||||
self.assertIsNone(actual)
|
||||
|
||||
def test_standard_extraction_string(self):
|
||||
"""String variables are extracted and stored"""
|
||||
string_keys = [
|
||||
"comment",
|
||||
"url",
|
||||
"wiki",
|
||||
"version",
|
||||
"author",
|
||||
"date",
|
||||
"icon",
|
||||
"xpm",
|
||||
]
|
||||
for key in string_keys:
|
||||
with self.subTest(key=key):
|
||||
self.test_object._standard_extraction(key, "test")
|
||||
self.assertEqual(self.test_object.parse_results[key], "test")
|
||||
|
||||
def test_standard_extraction_list(self):
|
||||
"""List variable is extracted and stored"""
|
||||
key = "other_files"
|
||||
self.test_object._standard_extraction(key, "test1, test2, test3")
|
||||
self.assertIn("test1", self.test_object.parse_results[key])
|
||||
self.assertIn("test2", self.test_object.parse_results[key])
|
||||
self.assertIn("test3", self.test_object.parse_results[key])
|
||||
|
||||
def test_apply_special_handling_version(self):
|
||||
"""If the tag is __version__, apply our special handling"""
|
||||
self.test_object._reset_map()
|
||||
self.test_object._apply_special_handling("__version__", 42)
|
||||
self.assertNotIn("__version__", self.test_object.remaining_item_map)
|
||||
self.assertEqual(self.test_object.parse_results["version"], "42")
|
||||
|
||||
def test_apply_special_handling_not_version(self):
|
||||
"""If the tag is not __version__, raise an error"""
|
||||
self.test_object._reset_map()
|
||||
with self.assertRaises(SyntaxError):
|
||||
self.test_object._apply_special_handling("__not_version__", 42)
|
||||
self.assertIn("__version__", self.test_object.remaining_item_map)
|
||||
|
||||
def test_process_noncompliant_version_date(self):
|
||||
"""Detect and allow __date__ for the __version__"""
|
||||
self.test_object.parse_results["date"] = "1/2/3"
|
||||
self.test_object._process_noncompliant_version("__date__")
|
||||
self.assertEqual(
|
||||
self.test_object.parse_results["version"],
|
||||
self.test_object.parse_results["date"],
|
||||
)
|
||||
|
||||
def test_process_noncompliant_version_float(self):
|
||||
"""Detect and allow floats for the __version__"""
|
||||
self.test_object._process_noncompliant_version(1.2)
|
||||
self.assertEqual(self.test_object.parse_results["version"], "1.2")
|
||||
|
||||
def test_process_noncompliant_version_int(self):
|
||||
"""Detect and allow integers for the __version__"""
|
||||
self.test_object._process_noncompliant_version(42)
|
||||
self.assertEqual(self.test_object.parse_results["version"], "42")
|
||||
|
||||
def test_detect_illegal_content_prefixed_string(self):
|
||||
"""Detect and raise an error for various kinds of prefixed strings"""
|
||||
illegal_strings = [
|
||||
"f'Some fancy {thing}'",
|
||||
'f"Some fancy {thing}"',
|
||||
"r'Some fancy {thing}'",
|
||||
'r"Some fancy {thing}"',
|
||||
"u'Some fancy {thing}'",
|
||||
'u"Some fancy {thing}"',
|
||||
"fr'Some fancy {thing}'",
|
||||
'fr"Some fancy {thing}"',
|
||||
"rf'Some fancy {thing}'",
|
||||
'rf"Some fancy {thing}"',
|
||||
]
|
||||
for test_string in illegal_strings:
|
||||
with self.subTest(test_string=test_string):
|
||||
with self.assertRaises(SyntaxError):
|
||||
MacroParser._detect_illegal_content(test_string)
|
||||
|
||||
def test_detect_illegal_content_not_a_string(self):
|
||||
"""Detect and raise an error for (some) non-strings"""
|
||||
illegal_strings = [
|
||||
"no quotes",
|
||||
"do_stuff()",
|
||||
'print("A function call sporting quotes!")',
|
||||
"__name__",
|
||||
"__version__",
|
||||
"1.2.3",
|
||||
]
|
||||
for test_string in illegal_strings:
|
||||
with self.subTest(test_string=test_string):
|
||||
with self.assertRaises(SyntaxError):
|
||||
MacroParser._detect_illegal_content(test_string)
|
||||
|
||||
def test_detect_illegal_content_no_failure(self):
|
||||
"""Recognize strings of various kinds, plus ints, and floats"""
|
||||
legal_strings = [
|
||||
'"Some legal value in double quotes"',
|
||||
"'Some legal value in single quotes'",
|
||||
'"""Some legal value in triple quotes"""',
|
||||
"__date__",
|
||||
"42",
|
||||
"4.2",
|
||||
]
|
||||
for test_string in legal_strings:
|
||||
with self.subTest(test_string=test_string):
|
||||
MacroParser._detect_illegal_content(test_string)
|
||||
|
||||
#####################
|
||||
# INTEGRATION TESTS #
|
||||
#####################
|
||||
|
||||
def test_macro_parser(self):
|
||||
"""INTEGRATION TEST: Given "real" data, ensure the parsing yields the expected results."""
|
||||
data_dir = os.path.join(os.path.dirname(__file__), "../data")
|
||||
macro_file = os.path.join(data_dir, "DoNothing.FCMacro")
|
||||
with open(macro_file, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
self.test_object.fill_details_from_code(code)
|
||||
self.assertEqual(len(self.test_object.console.errors), 0)
|
||||
self.assertEqual(len(self.test_object.console.warnings), 0)
|
||||
self.assertEqual(self.test_object.parse_results["author"], "Chris Hennes")
|
||||
self.assertEqual(self.test_object.parse_results["version"], "1.0")
|
||||
self.assertEqual(self.test_object.parse_results["date"], "2022-02-28")
|
||||
self.assertEqual(
|
||||
self.test_object.parse_results["comment"],
|
||||
"Do absolutely nothing. For Addon Manager integration tests.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.test_object.parse_results["url"], "https://github.com/FreeCAD/FreeCAD"
|
||||
)
|
||||
self.assertEqual(self.test_object.parse_results["icon"], "not_real.png")
|
||||
self.assertListEqual(
|
||||
self.test_object.parse_results["other_files"],
|
||||
["file1.py", "file2.py", "file3.py"],
|
||||
)
|
||||
self.assertNotEqual(self.test_object.parse_results["xpm"], "")
|
||||
@@ -1,649 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
Mock = unittest.mock.MagicMock
|
||||
|
||||
sys.path.append("../../")
|
||||
|
||||
|
||||
class TestVersion(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if "addonmanager_metadata" in sys.modules:
|
||||
sys.modules.pop("addonmanager_metadata")
|
||||
self.packaging_version = None
|
||||
if "packaging.version" in sys.modules:
|
||||
self.packaging_version = sys.modules["packaging.version"]
|
||||
sys.modules.pop("packaging.version")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self.packaging_version is not None:
|
||||
sys.modules["packaging.version"] = self.packaging_version
|
||||
|
||||
def test_init_from_string_manual(self):
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
version = amm.Version()
|
||||
version._parse_string_to_tuple = unittest.mock.MagicMock()
|
||||
version._init_from_string("1.2.3beta")
|
||||
self.assertTrue(version._parse_string_to_tuple.called)
|
||||
|
||||
def test_init_from_list_good(self):
|
||||
"""Initialization from a list works for good input"""
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
test_cases = [
|
||||
{"input": (1,), "output": [1, 0, 0, ""]},
|
||||
{"input": (1, 2), "output": [1, 2, 0, ""]},
|
||||
{"input": (1, 2, 3), "output": [1, 2, 3, ""]},
|
||||
{"input": (1, 2, 3, "b1"), "output": [1, 2, 3, "b1"]},
|
||||
]
|
||||
for test_case in test_cases:
|
||||
with self.subTest(test_case=test_case):
|
||||
v = amm.Version(from_list=test_case["input"])
|
||||
self.assertListEqual(test_case["output"], v.version_as_list)
|
||||
|
||||
def test_parse_string_to_tuple_normal(self):
|
||||
"""Parsing of complete version string works for normal cases"""
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
cases = {
|
||||
"1": [1, 0, 0, ""],
|
||||
"1.2": [1, 2, 0, ""],
|
||||
"1.2.3": [1, 2, 3, ""],
|
||||
"1.2.3beta": [1, 2, 3, "beta"],
|
||||
"12_345.6_7.8pre-alpha": [12345, 67, 8, "pre-alpha"],
|
||||
# The above test is mostly to point out that Python gets permits underscore
|
||||
# characters in a number.
|
||||
}
|
||||
for inp, output in cases.items():
|
||||
with self.subTest(inp=inp, output=output):
|
||||
version = amm.Version()
|
||||
version._parse_string_to_tuple(inp)
|
||||
self.assertListEqual(version.version_as_list, output)
|
||||
|
||||
def test_parse_string_to_tuple_invalid(self):
|
||||
"""Parsing of invalid version string raises an exception"""
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
cases = {"One", "1,2,3", "1-2-3", "1/2/3"}
|
||||
for inp in cases:
|
||||
with self.subTest(inp=inp):
|
||||
with self.assertRaises(ValueError):
|
||||
version = amm.Version()
|
||||
version._parse_string_to_tuple(inp)
|
||||
|
||||
def test_parse_final_entry_normal(self):
|
||||
"""Parsing of the final entry works for normal cases"""
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
cases = {
|
||||
"3beta": (3, "beta"),
|
||||
"42.alpha": (42, ".alpha"),
|
||||
"123.45.6": (123, ".45.6"),
|
||||
"98_delta": (98, "_delta"),
|
||||
"1 and some words": (1, " and some words"),
|
||||
}
|
||||
for inp, output in cases.items():
|
||||
with self.subTest(inp=inp, output=output):
|
||||
number, text = amm.Version._parse_final_entry(inp)
|
||||
self.assertEqual(number, output[0])
|
||||
self.assertEqual(text, output[1])
|
||||
|
||||
def test_parse_final_entry_invalid(self):
|
||||
"""Invalid input raises an exception"""
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
cases = ["beta", "", ["a", "b"]]
|
||||
for case in cases:
|
||||
with self.subTest(case=case):
|
||||
with self.assertRaises(ValueError):
|
||||
amm.Version._parse_final_entry(case)
|
||||
|
||||
def test_operators_internal(self):
|
||||
"""Test internal (non-package) comparison operators"""
|
||||
sys.modules["packaging.version"] = None
|
||||
import addonmanager_metadata as amm
|
||||
|
||||
cases = self.given_comparison_cases()
|
||||
for case in cases:
|
||||
with self.subTest(case=case):
|
||||
first = amm.Version(case[0])
|
||||
second = amm.Version(case[1])
|
||||
self.assertEqual(first < second, case[0] < case[1])
|
||||
self.assertEqual(first > second, case[0] > case[1])
|
||||
self.assertEqual(first <= second, case[0] <= case[1])
|
||||
self.assertEqual(first >= second, case[0] >= case[1])
|
||||
self.assertEqual(first == second, case[0] == case[1])
|
||||
|
||||
@staticmethod
|
||||
def given_comparison_cases():
|
||||
return [
|
||||
("0.0.0alpha", "1.0.0alpha"),
|
||||
("0.0.0alpha", "0.1.0alpha"),
|
||||
("0.0.0alpha", "0.0.1alpha"),
|
||||
("0.0.0alpha", "0.0.0beta"),
|
||||
("0.0.0alpha", "0.0.0alpha"),
|
||||
("1.0.0alpha", "0.0.0alpha"),
|
||||
("0.1.0alpha", "0.0.0alpha"),
|
||||
("0.0.1alpha", "0.0.0alpha"),
|
||||
("0.0.0beta", "0.0.0alpha"),
|
||||
]
|
||||
|
||||
|
||||
class TestDependencyType(unittest.TestCase):
|
||||
"""Ensure that the DependencyType dataclass converts to the correct strings"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
from addonmanager_metadata import DependencyType
|
||||
|
||||
self.DependencyType = DependencyType
|
||||
|
||||
def test_string_conversion_automatic(self):
|
||||
self.assertEqual(str(self.DependencyType.automatic), "automatic")
|
||||
|
||||
def test_string_conversion_internal(self):
|
||||
self.assertEqual(str(self.DependencyType.internal), "internal")
|
||||
|
||||
def test_string_conversion_addon(self):
|
||||
self.assertEqual(str(self.DependencyType.addon), "addon")
|
||||
|
||||
def test_string_conversion_python(self):
|
||||
self.assertEqual(str(self.DependencyType.python), "python")
|
||||
|
||||
|
||||
class TestUrlType(unittest.TestCase):
|
||||
"""Ensure that the UrlType dataclass converts to the correct strings"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
from addonmanager_metadata import UrlType
|
||||
|
||||
self.UrlType = UrlType
|
||||
|
||||
def test_string_conversion_website(self):
|
||||
self.assertEqual(str(self.UrlType.website), "website")
|
||||
|
||||
def test_string_conversion_repository(self):
|
||||
self.assertEqual(str(self.UrlType.repository), "repository")
|
||||
|
||||
def test_string_conversion_bugtracker(self):
|
||||
self.assertEqual(str(self.UrlType.bugtracker), "bugtracker")
|
||||
|
||||
def test_string_conversion_readme(self):
|
||||
self.assertEqual(str(self.UrlType.readme), "readme")
|
||||
|
||||
def test_string_conversion_documentation(self):
|
||||
self.assertEqual(str(self.UrlType.documentation), "documentation")
|
||||
|
||||
def test_string_conversion_discussion(self):
|
||||
self.assertEqual(str(self.UrlType.discussion), "discussion")
|
||||
|
||||
|
||||
class TestMetadataAuxiliaryFunctions(unittest.TestCase):
|
||||
def test_get_first_supported_freecad_version_simple(self):
|
||||
from addonmanager_metadata import (
|
||||
Metadata,
|
||||
Version,
|
||||
get_first_supported_freecad_version,
|
||||
)
|
||||
|
||||
expected_result = Version(from_string="0.20.2beta")
|
||||
metadata = self.given_metadata_with_freecadmin_set(expected_result)
|
||||
first_version = get_first_supported_freecad_version(metadata)
|
||||
self.assertEqual(expected_result, first_version)
|
||||
|
||||
@staticmethod
|
||||
def given_metadata_with_freecadmin_set(min_version):
|
||||
from addonmanager_metadata import Metadata
|
||||
|
||||
metadata = Metadata()
|
||||
metadata.freecadmin = min_version
|
||||
return metadata
|
||||
|
||||
def test_get_first_supported_freecad_version_with_content(self):
|
||||
from addonmanager_metadata import (
|
||||
Metadata,
|
||||
Version,
|
||||
get_first_supported_freecad_version,
|
||||
)
|
||||
|
||||
expected_result = Version(from_string="0.20.2beta")
|
||||
metadata = self.given_metadata_with_freecadmin_in_content(expected_result)
|
||||
first_version = get_first_supported_freecad_version(metadata)
|
||||
self.assertEqual(expected_result, first_version)
|
||||
|
||||
@staticmethod
|
||||
def given_metadata_with_freecadmin_in_content(min_version):
|
||||
from addonmanager_metadata import Metadata, Version
|
||||
|
||||
v_list = min_version.version_as_list
|
||||
metadata = Metadata()
|
||||
wb1 = Metadata()
|
||||
wb1.freecadmin = Version(from_list=[v_list[0] + 1, v_list[1], v_list[2], v_list[3]])
|
||||
wb2 = Metadata()
|
||||
wb2.freecadmin = Version(from_list=[v_list[0], v_list[1] + 1, v_list[2], v_list[3]])
|
||||
wb3 = Metadata()
|
||||
wb3.freecadmin = Version(from_list=[v_list[0], v_list[1], v_list[2] + 1, v_list[3]])
|
||||
m1 = Metadata()
|
||||
m1.freecadmin = min_version
|
||||
metadata.content = {"workbench": [wb1, wb2, wb3], "macro": [m1]}
|
||||
return metadata
|
||||
|
||||
|
||||
class TestMetadataReader(unittest.TestCase):
|
||||
"""Test reading metadata from XML"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
if "xml.etree.ElementTree" in sys.modules:
|
||||
sys.modules.pop("xml.etree.ElementTree")
|
||||
if "addonmanager_metadata" in sys.modules:
|
||||
sys.modules.pop("addonmanager_metadata")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if "xml.etree.ElementTree" in sys.modules:
|
||||
sys.modules.pop("xml.etree.ElementTree")
|
||||
if "addonmanager_metadata" in sys.modules:
|
||||
sys.modules.pop("addonmanager_metadata")
|
||||
|
||||
def test_from_file(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
MetadataReader.from_bytes = Mock()
|
||||
with tempfile.NamedTemporaryFile(delete=False) as temp:
|
||||
temp.write(b"Some data")
|
||||
temp.close()
|
||||
MetadataReader.from_file(temp.name)
|
||||
self.assertTrue(MetadataReader.from_bytes.called)
|
||||
MetadataReader.from_bytes.assert_called_once_with(b"Some data")
|
||||
os.unlink(temp.name)
|
||||
|
||||
@unittest.skip("Breaks other tests, needs to be fixed")
|
||||
def test_from_bytes(self):
|
||||
import xml.etree.ElementTree
|
||||
|
||||
with unittest.mock.patch("xml.etree.ElementTree") as element_tree_mock:
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
MetadataReader._process_element_tree = Mock()
|
||||
MetadataReader.from_bytes(b"Some data")
|
||||
element_tree_mock.parse.assert_called_once_with(b"Some data")
|
||||
|
||||
def test_process_element_tree(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
MetadataReader._determine_namespace = Mock(return_value="")
|
||||
element_tree_mock = Mock()
|
||||
MetadataReader._create_node = Mock()
|
||||
MetadataReader._process_element_tree(element_tree_mock)
|
||||
MetadataReader._create_node.assert_called_once()
|
||||
|
||||
def test_determine_namespace_found_full(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
root = Mock()
|
||||
root.tag = "{https://wiki.freecad.org/Package_Metadata}package"
|
||||
found_ns = MetadataReader._determine_namespace(root)
|
||||
self.assertEqual(found_ns, "{https://wiki.freecad.org/Package_Metadata}")
|
||||
|
||||
def test_determine_namespace_found_empty(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
root = Mock()
|
||||
root.tag = "package"
|
||||
found_ns = MetadataReader._determine_namespace(root)
|
||||
self.assertEqual(found_ns, "")
|
||||
|
||||
def test_determine_namespace_not_found(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
root = Mock()
|
||||
root.find = Mock(return_value=False)
|
||||
with self.assertRaises(RuntimeError):
|
||||
MetadataReader._determine_namespace(root)
|
||||
|
||||
def test_parse_child_element_simple_strings(self):
|
||||
from addonmanager_metadata import Metadata, MetadataReader
|
||||
|
||||
tags = ["name", "date", "description", "icon", "classname", "subdirectory"]
|
||||
for tag in tags:
|
||||
with self.subTest(tag=tag):
|
||||
text = f"Test Data for {tag}"
|
||||
child = self.given_mock_tree_node(tag, text)
|
||||
mock_metadata = Metadata()
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(mock_metadata.__dict__[tag], text)
|
||||
|
||||
def test_parse_child_element_version(self):
|
||||
from addonmanager_metadata import Metadata, Version, MetadataReader
|
||||
|
||||
mock_metadata = Metadata()
|
||||
child = self.given_mock_tree_node("version", "1.2.3")
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(Version("1.2.3"), mock_metadata.version)
|
||||
|
||||
def test_parse_child_element_version_bad(self):
|
||||
from addonmanager_metadata import Metadata, Version, MetadataReader
|
||||
|
||||
mock_metadata = Metadata()
|
||||
child = self.given_mock_tree_node("version", "1-2-3")
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(Version("0.0.0"), mock_metadata.version)
|
||||
|
||||
def test_parse_child_element_lists_of_strings(self):
|
||||
from addonmanager_metadata import Metadata, MetadataReader
|
||||
|
||||
tags = ["file", "tag"]
|
||||
for tag in tags:
|
||||
with self.subTest(tag=tag):
|
||||
mock_metadata = Metadata()
|
||||
expected_results = []
|
||||
for i in range(10):
|
||||
text = f"Test {i} for {tag}"
|
||||
expected_results.append(text)
|
||||
child = self.given_mock_tree_node(tag, text)
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(len(mock_metadata.__dict__[tag]), 10)
|
||||
self.assertListEqual(mock_metadata.__dict__[tag], expected_results)
|
||||
|
||||
def test_parse_child_element_lists_of_contacts(self):
|
||||
from addonmanager_metadata import Metadata, Contact, MetadataReader
|
||||
|
||||
tags = ["maintainer", "author"]
|
||||
for tag in tags:
|
||||
with self.subTest(tag=tag):
|
||||
mock_metadata = Metadata()
|
||||
expected_results = []
|
||||
for i in range(10):
|
||||
text = f"Test {i} for {tag}"
|
||||
email = f"Email {i} for {tag}" if i % 2 == 0 else None
|
||||
expected_results.append(Contact(name=text, email=email))
|
||||
child = self.given_mock_tree_node(tag, text, {"email": email})
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(len(mock_metadata.__dict__[tag]), 10)
|
||||
self.assertListEqual(mock_metadata.__dict__[tag], expected_results)
|
||||
|
||||
def test_parse_child_element_list_of_licenses(self):
|
||||
from addonmanager_metadata import Metadata, License, MetadataReader
|
||||
|
||||
mock_metadata = Metadata()
|
||||
expected_results = []
|
||||
tag = "license"
|
||||
for i in range(10):
|
||||
text = f"Test {i} for {tag}"
|
||||
file = f"Filename {i} for {tag}" if i % 2 == 0 else None
|
||||
expected_results.append(License(name=text, file=file))
|
||||
child = self.given_mock_tree_node(tag, text, {"file": file})
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(len(mock_metadata.__dict__[tag]), 10)
|
||||
self.assertListEqual(mock_metadata.__dict__[tag], expected_results)
|
||||
|
||||
def test_parse_child_element_list_of_urls(self):
|
||||
from addonmanager_metadata import Metadata, Url, UrlType, MetadataReader
|
||||
|
||||
mock_metadata = Metadata()
|
||||
expected_results = []
|
||||
tag = "url"
|
||||
for i in range(10):
|
||||
text = f"Test {i} for {tag}"
|
||||
url_type = UrlType(i % len(UrlType))
|
||||
type = str(url_type)
|
||||
branch = ""
|
||||
if type == "repository":
|
||||
branch = f"Branch {i} for {tag}"
|
||||
expected_results.append(Url(location=text, type=url_type, branch=branch))
|
||||
child = self.given_mock_tree_node(tag, text, {"type": type, "branch": branch})
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(len(mock_metadata.__dict__[tag]), 10)
|
||||
self.assertListEqual(mock_metadata.__dict__[tag], expected_results)
|
||||
|
||||
def test_parse_child_element_lists_of_dependencies(self):
|
||||
from addonmanager_metadata import (
|
||||
Metadata,
|
||||
Dependency,
|
||||
DependencyType,
|
||||
MetadataReader,
|
||||
)
|
||||
|
||||
tags = ["depend", "conflict", "replace"]
|
||||
attributes = {
|
||||
"version_lt": "1.0.0",
|
||||
"version_lte": "1.0.0",
|
||||
"version_eq": "1.0.0",
|
||||
"version_gte": "1.0.0",
|
||||
"version_gt": "1.0.0",
|
||||
"condition": "$BuildVersionMajor<1",
|
||||
"optional": True,
|
||||
}
|
||||
|
||||
for tag in tags:
|
||||
for attribute, attr_value in attributes.items():
|
||||
with self.subTest(tag=tag, attribute=attribute):
|
||||
mock_metadata = Metadata()
|
||||
expected_results = []
|
||||
for i in range(10):
|
||||
text = f"Test {i} for {tag}"
|
||||
dependency_type = DependencyType(i % len(DependencyType))
|
||||
dependency_type_str = str(dependency_type)
|
||||
expected = Dependency(package=text, dependency_type=dependency_type)
|
||||
expected.__dict__[attribute] = attr_value
|
||||
expected_results.append(expected)
|
||||
child = self.given_mock_tree_node(
|
||||
tag,
|
||||
text,
|
||||
{"type": dependency_type_str, attribute: str(attr_value)},
|
||||
)
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(len(mock_metadata.__dict__[tag]), 10)
|
||||
self.assertListEqual(mock_metadata.__dict__[tag], expected_results)
|
||||
|
||||
def test_parse_child_element_ignore_unknown_tag(self):
|
||||
from addonmanager_metadata import Metadata, MetadataReader
|
||||
|
||||
tag = "invalid_tag"
|
||||
text = "Shouldn't matter"
|
||||
child = self.given_mock_tree_node(tag, text)
|
||||
mock_metadata = Metadata()
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertNotIn(tag, mock_metadata.__dict__)
|
||||
|
||||
def test_parse_child_element_versions(self):
|
||||
from addonmanager_metadata import Metadata, Version, MetadataReader
|
||||
|
||||
tags = ["version", "freecadmin", "freecadmax", "pythonmin"]
|
||||
for tag in tags:
|
||||
with self.subTest(tag=tag):
|
||||
mock_metadata = Metadata()
|
||||
text = "3.4.5beta"
|
||||
child = self.given_mock_tree_node(tag, text)
|
||||
MetadataReader._parse_child_element("", child, mock_metadata)
|
||||
self.assertEqual(mock_metadata.__dict__[tag], Version(from_string=text))
|
||||
|
||||
def given_mock_tree_node(self, tag, text, attributes=None):
|
||||
class MockTreeNode:
|
||||
def __init__(self):
|
||||
self.tag = tag
|
||||
self.text = text
|
||||
self.attrib = attributes if attributes is not None else []
|
||||
|
||||
return MockTreeNode()
|
||||
|
||||
def test_parse_content_valid(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
valid_content_items = ["workbench", "macro", "preferencepack"]
|
||||
MetadataReader._create_node = Mock()
|
||||
for content_type in valid_content_items:
|
||||
with self.subTest(content_type=content_type):
|
||||
tree_mock = [self.given_mock_tree_node(content_type, None)]
|
||||
metadata_mock = Mock()
|
||||
MetadataReader._parse_content("", metadata_mock, tree_mock)
|
||||
MetadataReader._create_node.assert_called_once()
|
||||
MetadataReader._create_node.reset_mock()
|
||||
|
||||
def test_parse_content_invalid(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
MetadataReader._create_node = Mock()
|
||||
content_item = "no_such_content_type"
|
||||
tree_mock = [self.given_mock_tree_node(content_item, None)]
|
||||
metadata_mock = Mock()
|
||||
MetadataReader._parse_content("", metadata_mock, tree_mock)
|
||||
MetadataReader._create_node.assert_not_called()
|
||||
|
||||
|
||||
class TestMetadataReaderIntegration(unittest.TestCase):
|
||||
"""Full-up tests of the MetadataReader class (no mocking)."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.test_data_dir = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||
remove_list = []
|
||||
for key in sys.modules:
|
||||
if "addonmanager_metadata" in key:
|
||||
remove_list.append(key)
|
||||
for key in remove_list:
|
||||
print(f"Removing {key}")
|
||||
sys.modules.pop(key)
|
||||
|
||||
def test_loading_simple_metadata_file(self):
|
||||
from addonmanager_metadata import (
|
||||
Contact,
|
||||
Dependency,
|
||||
License,
|
||||
MetadataReader,
|
||||
Url,
|
||||
UrlType,
|
||||
Version,
|
||||
)
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "good_package.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertEqual("Test Workbench", metadata.name)
|
||||
self.assertEqual("A package.xml file for unit testing.", metadata.description)
|
||||
self.assertEqual(Version("1.0.1"), metadata.version)
|
||||
self.assertEqual("2022-01-07", metadata.date)
|
||||
self.assertEqual("Resources/icons/PackageIcon.svg", metadata.icon)
|
||||
self.assertListEqual([License(name="LGPL-2.1", file="LICENSE")], metadata.license)
|
||||
self.assertListEqual(
|
||||
[Contact(name="FreeCAD Developer", email="developer@freecad.org")],
|
||||
metadata.maintainer,
|
||||
)
|
||||
self.assertListEqual(
|
||||
[
|
||||
Url(
|
||||
location="https://github.com/chennes/FreeCAD-Package",
|
||||
type=UrlType.repository,
|
||||
branch="main",
|
||||
),
|
||||
Url(
|
||||
location="https://github.com/chennes/FreeCAD-Package/blob/main/README.md",
|
||||
type=UrlType.readme,
|
||||
),
|
||||
],
|
||||
metadata.url,
|
||||
)
|
||||
self.assertListEqual(["Tag0", "Tag1"], metadata.tag)
|
||||
self.assertIn("workbench", metadata.content)
|
||||
self.assertEqual(len(metadata.content["workbench"]), 1)
|
||||
wb_metadata = metadata.content["workbench"][0]
|
||||
self.assertEqual("MyWorkbench", wb_metadata.classname)
|
||||
self.assertEqual("./", wb_metadata.subdirectory)
|
||||
self.assertListEqual(["TagA", "TagB", "TagC"], wb_metadata.tag)
|
||||
|
||||
def test_multiple_workbenches(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "workbench_only.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertIn("workbench", metadata.content)
|
||||
self.assertEqual(len(metadata.content["workbench"]), 3)
|
||||
expected_wb_classnames = [
|
||||
"MyFirstWorkbench",
|
||||
"MySecondWorkbench",
|
||||
"MyThirdWorkbench",
|
||||
]
|
||||
for wb in metadata.content["workbench"]:
|
||||
self.assertIn(wb.classname, expected_wb_classnames)
|
||||
expected_wb_classnames.remove(wb.classname)
|
||||
self.assertEqual(len(expected_wb_classnames), 0)
|
||||
|
||||
def test_multiple_macros(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "macro_only.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertIn("macro", metadata.content)
|
||||
self.assertEqual(len(metadata.content["macro"]), 2)
|
||||
expected_wb_files = ["MyMacro.FCStd", "MyOtherMacro.FCStd"]
|
||||
for wb in metadata.content["macro"]:
|
||||
self.assertIn(wb.file[0], expected_wb_files)
|
||||
expected_wb_files.remove(wb.file[0])
|
||||
self.assertEqual(len(expected_wb_files), 0)
|
||||
|
||||
def test_multiple_preference_packs(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "prefpack_only.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertIn("preferencepack", metadata.content)
|
||||
self.assertEqual(len(metadata.content["preferencepack"]), 3)
|
||||
expected_packs = ["MyFirstPack", "MySecondPack", "MyThirdPack"]
|
||||
for wb in metadata.content["preferencepack"]:
|
||||
self.assertIn(wb.name, expected_packs)
|
||||
expected_packs.remove(wb.name)
|
||||
self.assertEqual(len(expected_packs), 0)
|
||||
|
||||
def test_bundle(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "bundle_only.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertIn("bundle", metadata.content)
|
||||
self.assertEqual(len(metadata.content["bundle"]), 1)
|
||||
|
||||
def test_other(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "other_only.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertIn("other", metadata.content)
|
||||
self.assertEqual(len(metadata.content["other"]), 1)
|
||||
|
||||
def test_content_combination(self):
|
||||
from addonmanager_metadata import MetadataReader
|
||||
|
||||
filename = os.path.join(self.test_data_dir, "combination.xml")
|
||||
metadata = MetadataReader.from_file(filename)
|
||||
self.assertIn("preferencepack", metadata.content)
|
||||
self.assertEqual(len(metadata.content["preferencepack"]), 1)
|
||||
self.assertIn("macro", metadata.content)
|
||||
self.assertEqual(len(metadata.content["macro"]), 1)
|
||||
self.assertIn("workbench", metadata.content)
|
||||
self.assertEqual(len(metadata.content["workbench"]), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,437 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Contains the unit test class for addonmanager_uninstaller.py non-GUI functionality."""
|
||||
|
||||
import functools
|
||||
import os
|
||||
from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from addonmanager_uninstaller import AddonUninstaller, MacroUninstaller
|
||||
|
||||
from Addon import Addon
|
||||
from AddonManagerTest.app.mocks import MockAddon, MockMacro
|
||||
|
||||
|
||||
class TestAddonUninstaller(unittest.TestCase):
|
||||
"""Test class for addonmanager_uninstaller.py non-GUI functionality"""
|
||||
|
||||
MODULE = "test_uninstaller" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize data needed for all tests"""
|
||||
self.test_data_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
self.mock_addon = MockAddon()
|
||||
self.signals_caught = []
|
||||
self.test_object = AddonUninstaller(self.mock_addon)
|
||||
|
||||
self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))
|
||||
self.test_object.success.connect(functools.partial(self.catch_signal, "success"))
|
||||
self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))
|
||||
|
||||
def tearDown(self):
|
||||
"""Finalize the test."""
|
||||
|
||||
def catch_signal(self, signal_name, *_):
|
||||
"""Internal use: used to catch and log any emitted signals. Not called directly."""
|
||||
self.signals_caught.append(signal_name)
|
||||
|
||||
def setup_dummy_installation(self, temp_dir) -> str:
|
||||
"""Set up a dummy Addon in temp_dir"""
|
||||
toplevel_path = os.path.join(temp_dir, self.mock_addon.name)
|
||||
os.makedirs(toplevel_path)
|
||||
with open(os.path.join(toplevel_path, "README.md"), "w") as f:
|
||||
f.write("## Mock Addon ##\n\nFile created by the unit test code.")
|
||||
self.test_object.installation_path = temp_dir
|
||||
return toplevel_path
|
||||
|
||||
def create_fake_macro(self, macro_directory, fake_macro_name, digest):
|
||||
"""Create an FCMacro file and matching digest entry for later removal"""
|
||||
os.makedirs(macro_directory, exist_ok=True)
|
||||
fake_file_installed = os.path.join(macro_directory, fake_macro_name)
|
||||
with open(digest, "a", encoding="utf-8") as f:
|
||||
f.write("# The following files were created outside this installation:\n")
|
||||
f.write(fake_file_installed + "\n")
|
||||
with open(fake_file_installed, "w", encoding="utf-8") as f:
|
||||
f.write("# Fake macro data for unit testing")
|
||||
|
||||
def test_uninstall_normal(self):
|
||||
"""Test the integrated uninstall function under normal circumstances"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
self.test_object.run()
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
self.assertFalse(os.path.exists(toplevel_path))
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_uninstall_no_name(self):
|
||||
"""Test the integrated uninstall function for an addon without a name"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
self.mock_addon.name = None
|
||||
result = self.test_object.run()
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
self.assertIn("failure", self.signals_caught)
|
||||
self.assertNotIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_uninstall_dangerous_name(self):
|
||||
"""Test the integrated uninstall function for an addon with a dangerous name"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
self.mock_addon.name = "./"
|
||||
result = self.test_object.run()
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
self.assertIn("failure", self.signals_caught)
|
||||
self.assertNotIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_uninstall_unmatching_name(self):
|
||||
"""Test the integrated uninstall function for an addon with a name that isn't installed"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
self.mock_addon.name += "Nonexistent"
|
||||
result = self.test_object.run()
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
self.assertIn("failure", self.signals_caught)
|
||||
self.assertNotIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_uninstall_addon_with_macros(self):
|
||||
"""Tests that the uninstaller removes the macro files"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
macro_directory = os.path.join(temp_dir, "Macros")
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
result = self.test_object.run()
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro.FCMacro")))
|
||||
self.assertTrue(os.path.exists(macro_directory))
|
||||
|
||||
def test_uninstall_calls_script(self):
|
||||
"""Tests that the main uninstaller run function calls the uninstall.py script"""
|
||||
|
||||
class Interceptor:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.args = []
|
||||
|
||||
def func(self, *args):
|
||||
self.called = True
|
||||
self.args = args
|
||||
|
||||
interceptor = Interceptor()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
self.test_object.run_uninstall_script = interceptor.func
|
||||
result = self.test_object.run()
|
||||
self.assertTrue(interceptor.called, "Failed to call uninstall script")
|
||||
|
||||
def test_remove_extra_files_no_digest(self):
|
||||
"""Tests that a lack of digest file is not an error, and nothing gets removed"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.remove_extra_files(temp_dir) # Shouldn't throw
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
|
||||
def test_remove_extra_files_empty_digest(self):
|
||||
"""Test that an empty digest file is not an error, and nothing gets removed"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with open("AM_INSTALLATION_DIGEST.txt", "w", encoding="utf-8") as f:
|
||||
f.write("")
|
||||
self.test_object.remove_extra_files(temp_dir) # Shouldn't throw
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
|
||||
def test_remove_extra_files_comment_only_digest(self):
|
||||
"""Test that a digest file that contains only comment lines is not an error, and nothing
|
||||
gets removed"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with open("AM_INSTALLATION_DIGEST.txt", "w", encoding="utf-8") as f:
|
||||
f.write("# Fake digest file for unit testing")
|
||||
self.test_object.remove_extra_files(temp_dir) # Shouldn't throw
|
||||
self.assertTrue(os.path.exists(temp_dir))
|
||||
|
||||
def test_remove_extra_files_repeated_files(self):
|
||||
"""Test that a digest with the same file repeated removes it once, but doesn't error on
|
||||
later requests to remove it."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
macro_directory = os.path.join(temp_dir, "Macros")
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
self.test_object.remove_extra_files(toplevel_path) # Shouldn't throw
|
||||
self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro.FCMacro")))
|
||||
|
||||
def test_remove_extra_files_normal_case(self):
|
||||
"""Test that a digest that is a "normal" case removes the requested files"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
macro_directory = os.path.join(temp_dir, "Macros")
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro1.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro2.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
self.create_fake_macro(
|
||||
macro_directory,
|
||||
"FakeMacro3.FCMacro",
|
||||
os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
|
||||
)
|
||||
|
||||
# Make sure the setup worked as expected, otherwise the test is meaningless
|
||||
self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro1.FCMacro")))
|
||||
self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro2.FCMacro")))
|
||||
self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro3.FCMacro")))
|
||||
|
||||
self.test_object.remove_extra_files(toplevel_path) # Shouldn't throw
|
||||
|
||||
self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro1.FCMacro")))
|
||||
self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro2.FCMacro")))
|
||||
self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro3.FCMacro")))
|
||||
|
||||
def test_runs_uninstaller_script_successful(self):
|
||||
"""Tests that the uninstall.py script is called"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
with open(os.path.join(toplevel_path, "uninstall.py"), "w", encoding="utf-8") as f:
|
||||
double_escaped = temp_dir.replace("\\", "\\\\")
|
||||
f.write(
|
||||
f"""# Mock uninstaller script
|
||||
import os
|
||||
path = '{double_escaped}'
|
||||
with open(os.path.join(path,"RAN_UNINSTALLER.txt"),"w",encoding="utf-8") as f:
|
||||
f.write("File created by uninstall.py from unit tests")
|
||||
"""
|
||||
)
|
||||
self.test_object.run_uninstall_script(toplevel_path) # The exception does not leak out
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, "RAN_UNINSTALLER.txt")))
|
||||
|
||||
def test_runs_uninstaller_script_failure(self):
|
||||
"""Tests that exceptions in the uninstall.py script do not leak out"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
toplevel_path = self.setup_dummy_installation(temp_dir)
|
||||
with open(os.path.join(toplevel_path, "uninstall.py"), "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
f"""# Mock uninstaller script
|
||||
raise RuntimeError("Fake exception for unit testing")
|
||||
"""
|
||||
)
|
||||
self.test_object.run_uninstall_script(toplevel_path) # The exception does not leak out
|
||||
|
||||
|
||||
class TestMacroUninstaller(unittest.TestCase):
|
||||
"""Test class for addonmanager_uninstaller.py non-GUI functionality"""
|
||||
|
||||
MODULE = "test_uninstaller" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.mock_addon = MockAddon()
|
||||
self.mock_addon.macro = MockMacro()
|
||||
self.test_object = MacroUninstaller(self.mock_addon)
|
||||
self.signals_caught = []
|
||||
|
||||
self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))
|
||||
self.test_object.success.connect(functools.partial(self.catch_signal, "success"))
|
||||
self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def catch_signal(self, signal_name, *_):
|
||||
"""Internal use: used to catch and log any emitted signals. Not called directly."""
|
||||
self.signals_caught.append(signal_name)
|
||||
|
||||
def test_remove_simple_macro(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.installation_location = temp_dir
|
||||
self.mock_addon.macro.install(temp_dir)
|
||||
# Make sure the setup worked, otherwise the test is meaningless
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.test_object.run()
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_remove_macro_with_icon(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.installation_location = temp_dir
|
||||
self.mock_addon.macro.icon = "mock_icon_test.svg"
|
||||
self.mock_addon.macro.install(temp_dir)
|
||||
# Make sure the setup worked, otherwise the test is meaningless
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.icon)))
|
||||
self.test_object.run()
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.icon)))
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_remove_macro_with_xpm_data(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.installation_location = temp_dir
|
||||
self.mock_addon.macro.xpm = "/*Fake XPM data*/"
|
||||
self.mock_addon.macro.install(temp_dir)
|
||||
# Make sure the setup worked, otherwise the test is meaningless
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.assertTrue(os.path.exists(os.path.join(temp_dir, "MockMacro_icon.xpm")))
|
||||
self.test_object.run()
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, "MockMacro_icon.xpm")))
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_remove_macro_with_files(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.installation_location = temp_dir
|
||||
self.mock_addon.macro.other_files = [
|
||||
"test_file_1.txt",
|
||||
"test_file_2.FCMacro",
|
||||
"subdir/test_file_3.txt",
|
||||
]
|
||||
self.mock_addon.macro.install(temp_dir)
|
||||
# Make sure the setup worked, otherwise the test is meaningless
|
||||
for f in self.mock_addon.macro.other_files:
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, f)),
|
||||
f"Expected {f} to exist, and it does not",
|
||||
)
|
||||
self.test_object.run()
|
||||
for f in self.mock_addon.macro.other_files:
|
||||
self.assertFalse(
|
||||
os.path.exists(os.path.join(temp_dir, f)),
|
||||
f"Expected {f} to be removed, and it was not",
|
||||
)
|
||||
self.assertFalse(
|
||||
os.path.exists(os.path.join(temp_dir, "subdir")),
|
||||
"Failed to remove empty subdirectory",
|
||||
)
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_remove_nonexistent_macro(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.installation_location = temp_dir
|
||||
# Don't run the installer:
|
||||
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.test_object.run() # Should not raise an exception
|
||||
self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_remove_write_protected_macro(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.test_object.installation_location = temp_dir
|
||||
self.mock_addon.macro.install(temp_dir)
|
||||
# Make sure the setup worked, otherwise the test is meaningless
|
||||
f = os.path.join(temp_dir, self.mock_addon.macro.filename)
|
||||
self.assertTrue(os.path.exists(f))
|
||||
os.chmod(f, S_IREAD | S_IRGRP | S_IROTH)
|
||||
self.test_object.run()
|
||||
|
||||
if os.path.exists(f):
|
||||
os.chmod(f, S_IWUSR | S_IREAD)
|
||||
self.assertNotIn("success", self.signals_caught)
|
||||
self.assertIn("failure", self.signals_caught)
|
||||
else:
|
||||
# In some cases we managed to delete it anyway:
|
||||
self.assertIn("success", self.signals_caught)
|
||||
self.assertNotIn("failure", self.signals_caught)
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_cleanup_directories_multiple_empty(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
empty_directories = set(["empty1", "empty2", "empty3"])
|
||||
full_paths = set()
|
||||
for directory in empty_directories:
|
||||
full_path = os.path.join(temp_dir, directory)
|
||||
os.mkdir(full_path)
|
||||
full_paths.add(full_path)
|
||||
|
||||
for directory in full_paths:
|
||||
self.assertTrue(directory, "Test code failed to create {directory}")
|
||||
self.test_object._cleanup_directories(full_paths)
|
||||
for directory in full_paths:
|
||||
self.assertFalse(os.path.exists(directory))
|
||||
|
||||
def test_cleanup_directories_none(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
full_paths = set()
|
||||
self.test_object._cleanup_directories(full_paths) # Shouldn't throw
|
||||
|
||||
def test_cleanup_directories_not_empty(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
empty_directories = set(["empty1", "empty2", "empty3"])
|
||||
full_paths = set()
|
||||
for directory in empty_directories:
|
||||
full_path = os.path.join(temp_dir, directory)
|
||||
os.mkdir(full_path)
|
||||
full_paths.add(full_path)
|
||||
with open(os.path.join(full_path, "test.txt"), "w", encoding="utf-8") as f:
|
||||
f.write("Unit test dummy data\n")
|
||||
|
||||
for directory in full_paths:
|
||||
self.assertTrue(directory, "Test code failed to create {directory}")
|
||||
self.test_object._cleanup_directories(full_paths)
|
||||
for directory in full_paths:
|
||||
self.assertTrue(os.path.exists(directory))
|
||||
@@ -1,287 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from datetime import datetime
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
import FreeCAD
|
||||
except ImportError:
|
||||
FreeCAD = None
|
||||
|
||||
sys.path.append("../..")
|
||||
|
||||
from AddonManagerTest.app.mocks import MockAddon as Addon
|
||||
|
||||
from addonmanager_utilities import (
|
||||
get_assigned_string_literal,
|
||||
get_macro_version_from_file,
|
||||
get_readme_url,
|
||||
process_date_string_to_python_datetime,
|
||||
recognized_git_location,
|
||||
run_interruptable_subprocess,
|
||||
)
|
||||
|
||||
|
||||
class TestUtilities(unittest.TestCase):
|
||||
|
||||
MODULE = "test_utilities" # file name without extension
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
try:
|
||||
os.remove("AM_INSTALLATION_DIGEST.txt")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def test_recognized_git_location(self):
|
||||
recognized_urls = [
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
"https://gitlab.com/freecad/FreeCAD",
|
||||
"https://framagit.org/freecad/FreeCAD",
|
||||
"https://salsa.debian.org/science-team/freecad",
|
||||
]
|
||||
for url in recognized_urls:
|
||||
repo = Addon("Test Repo", url, "Addon.Status.NOT_INSTALLED", "branch")
|
||||
self.assertTrue(recognized_git_location(repo), f"{url} was unexpectedly not recognized")
|
||||
|
||||
unrecognized_urls = [
|
||||
"https://google.com",
|
||||
"https://freecad.org",
|
||||
"https://not.quite.github.com/FreeCAD/FreeCAD",
|
||||
"https://github.com.malware.com/",
|
||||
]
|
||||
for url in unrecognized_urls:
|
||||
repo = Addon("Test Repo", url, "Addon.Status.NOT_INSTALLED", "branch")
|
||||
self.assertFalse(recognized_git_location(repo), f"{url} was unexpectedly recognized")
|
||||
|
||||
def test_get_readme_url(self):
|
||||
github_urls = [
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
]
|
||||
gitlab_urls = [
|
||||
"https://gitlab.com/freecad/FreeCAD",
|
||||
"https://framagit.org/freecad/FreeCAD",
|
||||
"https://salsa.debian.org/science-team/freecad",
|
||||
"https://unknown.location/and/path",
|
||||
]
|
||||
|
||||
# GitHub and Gitlab have two different schemes for file URLs: unrecognized URLs are
|
||||
# presumed to be local instances of a GitLab server. Note that in neither case does this
|
||||
# take into account the redirects that are used to actually fetch the data.
|
||||
|
||||
for url in github_urls:
|
||||
branch = "branchname"
|
||||
expected_result = f"{url}/raw/{branch}/README.md"
|
||||
repo = Addon("Test Repo", url, "Addon.Status.NOT_INSTALLED", branch)
|
||||
actual_result = get_readme_url(repo)
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
for url in gitlab_urls:
|
||||
branch = "branchname"
|
||||
expected_result = f"{url}/-/raw/{branch}/README.md"
|
||||
repo = Addon("Test Repo", url, "Addon.Status.NOT_INSTALLED", branch)
|
||||
actual_result = get_readme_url(repo)
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
def test_get_assigned_string_literal(self):
|
||||
good_lines = [
|
||||
["my_var = 'Single-quoted literal'", "Single-quoted literal"],
|
||||
['my_var = "Double-quoted literal"', "Double-quoted literal"],
|
||||
["my_var = \t 'Extra whitespace'", "Extra whitespace"],
|
||||
["my_var = 42", "42"],
|
||||
["my_var = 1.23", "1.23"],
|
||||
]
|
||||
for line in good_lines:
|
||||
result = get_assigned_string_literal(line[0])
|
||||
self.assertEqual(result, line[1])
|
||||
|
||||
bad_lines = [
|
||||
"my_var = __date__",
|
||||
"my_var 'No equals sign'",
|
||||
"my_var = 'Unmatched quotes\"",
|
||||
"my_var = No quotes at all",
|
||||
"my_var = 1.2.3",
|
||||
]
|
||||
for line in bad_lines:
|
||||
result = get_assigned_string_literal(line)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_macro_version_from_file_good_metadata(self):
|
||||
good_metadata = """__Version__ = "1.2.3" """
|
||||
with patch("builtins.open", new_callable=mock_open, read_data=good_metadata):
|
||||
version = get_macro_version_from_file("mocked_file.FCStd")
|
||||
self.assertEqual(version, "1.2.3")
|
||||
|
||||
def test_get_macro_version_from_file_missing_quotes(self):
|
||||
bad_metadata = """__Version__ = 1.2.3 """ # No quotes
|
||||
with patch("builtins.open", new_callable=mock_open, read_data=bad_metadata):
|
||||
version = get_macro_version_from_file("mocked_file.FCStd")
|
||||
self.assertEqual(version, "", "Bad version did not yield empty string")
|
||||
|
||||
def test_get_macro_version_from_file_no_version(self):
|
||||
good_metadata = ""
|
||||
with patch("builtins.open", new_callable=mock_open, read_data=good_metadata):
|
||||
version = get_macro_version_from_file("mocked_file.FCStd")
|
||||
self.assertEqual(version, "", "Missing version did not yield empty string")
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_run_interruptable_subprocess_success_instant_return(self, mock_popen):
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = ("Mocked stdout", "Mocked stderr")
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
completed_process = run_interruptable_subprocess(["arg0", "arg1"])
|
||||
|
||||
self.assertEqual(completed_process.returncode, 0)
|
||||
self.assertEqual(completed_process.stdout, "Mocked stdout")
|
||||
self.assertEqual(completed_process.stderr, "Mocked stderr")
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_run_interruptable_subprocess_returns_nonzero(self, mock_popen):
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = ("Mocked stdout", "Mocked stderr")
|
||||
mock_process.returncode = 1
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
run_interruptable_subprocess(["arg0", "arg1"])
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_run_interruptable_subprocess_timeout_five_times(self, mock_popen):
|
||||
"""Five times is below the limit for an error to be raised"""
|
||||
|
||||
def raises_first_five_times(timeout):
|
||||
raises_first_five_times.counter += 1
|
||||
if raises_first_five_times.counter <= 5:
|
||||
raise subprocess.TimeoutExpired("Test", timeout)
|
||||
return "Mocked stdout", None
|
||||
|
||||
raises_first_five_times.counter = 0
|
||||
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate = raises_first_five_times
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
result = run_interruptable_subprocess(["arg0", "arg1"], 10)
|
||||
|
||||
self.assertEqual(result.returncode, 0)
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_run_interruptable_subprocess_timeout_exceeded(self, mock_popen):
|
||||
"""Exceeding the set timeout gives a CalledProcessError exception"""
|
||||
|
||||
def raises_one_time(timeout=0):
|
||||
if not raises_one_time.raised:
|
||||
raises_one_time.raised = True
|
||||
raise subprocess.TimeoutExpired("Test", timeout)
|
||||
return "Mocked stdout", None
|
||||
|
||||
raises_one_time.raised = False
|
||||
|
||||
def fake_time():
|
||||
"""Time that advances by one second every time it is called"""
|
||||
fake_time.time += 1.0
|
||||
return fake_time.time
|
||||
|
||||
fake_time.time = 0.0
|
||||
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate = raises_one_time
|
||||
raises_one_time.mock_access = mock_process
|
||||
mock_process.returncode = None
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
with patch("time.time", fake_time):
|
||||
run_interruptable_subprocess(["arg0", "arg1"], 0.1)
|
||||
|
||||
def test_process_date_string_to_python_datetime_non_numeric(self):
|
||||
with self.assertRaises(ValueError):
|
||||
process_date_string_to_python_datetime("TwentyTwentyFour-January-ThirtyFirst")
|
||||
|
||||
def test_process_date_string_to_python_datetime_year_first(self):
|
||||
result = process_date_string_to_python_datetime("2024-01-31")
|
||||
expected_result = datetime(2024, 1, 31, 0, 0)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_process_date_string_to_python_datetime_day_first(self):
|
||||
result = process_date_string_to_python_datetime("31-01-2024")
|
||||
expected_result = datetime(2024, 1, 31, 0, 0)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_process_date_string_to_python_datetime_month_first(self):
|
||||
result = process_date_string_to_python_datetime("01-31-2024")
|
||||
expected_result = datetime(2024, 1, 31, 0, 0)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_process_date_string_to_python_datetime_ambiguous(self):
|
||||
"""In the ambiguous case, the code should assume that the date is in the DD-MM-YYYY format."""
|
||||
result = process_date_string_to_python_datetime("01-12-2024")
|
||||
expected_result = datetime(2024, 12, 1, 0, 0)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_process_date_string_to_python_datetime_invalid_date(self):
|
||||
with self.assertRaises(ValueError):
|
||||
process_date_string_to_python_datetime("13-31-2024")
|
||||
|
||||
def test_process_date_string_to_python_datetime_too_many_components(self):
|
||||
with self.assertRaises(ValueError):
|
||||
process_date_string_to_python_datetime("01-01-31-2024")
|
||||
|
||||
def test_process_date_string_to_python_datetime_too_few_components(self):
|
||||
"""Month-Year-only dates are not supported"""
|
||||
with self.assertRaises(ValueError):
|
||||
process_date_string_to_python_datetime("01-2024")
|
||||
|
||||
def test_process_date_string_to_python_datetime_unrecognizable(self):
|
||||
"""Two-digit years are not supported"""
|
||||
with self.assertRaises(ValueError):
|
||||
process_date_string_to_python_datetime("01-02-24")
|
||||
|
||||
def test_process_date_string_to_python_datetime_valid_separators(self):
|
||||
"""Four individual separators are supported, plus any combination of multiple of those separators"""
|
||||
valid_separators = [" ", ".", "/", "-", " - ", " / ", "--"]
|
||||
for separator in valid_separators:
|
||||
with self.subTest(separator=separator):
|
||||
result = process_date_string_to_python_datetime(f"2024{separator}01{separator}31")
|
||||
expected_result = datetime(2024, 1, 31, 0, 0)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_process_date_string_to_python_datetime_invalid_separators(self):
|
||||
"""Only the four separators [ ./-] are supported: ensure others fail"""
|
||||
invalid_separators = ["a", "\\", "|", "'", ";", "*", " \\ "]
|
||||
for separator in invalid_separators:
|
||||
with self.subTest(separator=separator):
|
||||
with self.assertRaises(ValueError):
|
||||
process_date_string_to_python_datetime(f"2024{separator}01{separator}31")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__Title__ = 'Do Nothing'
|
||||
__Author__ = 'Chris Hennes'
|
||||
__Version__ = '1.0'
|
||||
__Date__ = '2022-02-28'
|
||||
__Comment__ = 'Do absolutely nothing. For Addon Manager integration tests.'
|
||||
__Web__ = 'https://github.com/FreeCAD/FreeCAD'
|
||||
__Wiki__ = ''
|
||||
__Icon__ = 'not_real.png'
|
||||
__Help__ = 'Not much to help with'
|
||||
__Status__ = 'Very Stable'
|
||||
__Requires__ = ''
|
||||
__Communication__ = 'Shout into the void'
|
||||
__Files__ = 'file1.py, file2.py, file3.py'
|
||||
__Xpm__ = """/* XPM */
|
||||
static char * blarg_xpm[] = {
|
||||
"16 7 2 1",
|
||||
"* c #000000",
|
||||
". c #ffffff",
|
||||
"**..*...........",
|
||||
"*.*.*...........",
|
||||
"**..*..**.**..**",
|
||||
"*.*.*.*.*.*..*.*",
|
||||
"**..*..**.*...**",
|
||||
"...............*",
|
||||
".............**."
|
||||
};"""
|
||||
|
||||
print("Well, not quite *nothing*... it does print this line out.")
|
||||
@@ -1,156 +0,0 @@
|
||||
{
|
||||
"3DfindIT": {
|
||||
"refs/remotes/origin/HEAD": [
|
||||
"2022-09-08T13:58:17+02:00",
|
||||
"dc99f8f1bdb17c1e55c00ac0dffa3ec15caf5b9d"
|
||||
],
|
||||
"refs/remotes/origin/master": [
|
||||
"2022-09-08T13:58:17+02:00",
|
||||
"dc99f8f1bdb17c1e55c00ac0dffa3ec15caf5b9d"
|
||||
],
|
||||
"refs/tags/v1.0": [
|
||||
"2020-11-09T10:11:44+01:00",
|
||||
"e97b6caf5eaed0b709dfeabecedac14c9bc2cc2b"
|
||||
],
|
||||
"refs/tags/v1.1": [
|
||||
"2021-06-21T13:33:58+02:00",
|
||||
"b9ab38ae93fcb8c1f69516ea563494bca63ebf49"
|
||||
],
|
||||
"refs/tags/v1.2": [
|
||||
"2021-08-31T07:42:11+02:00",
|
||||
"d8553961a43dd450681f6df6e8ce5bce72da1b0a"
|
||||
]
|
||||
},
|
||||
"3D_Printing_Tools": {
|
||||
"refs/remotes/origin/HEAD": [
|
||||
"2019-06-30T23:01:34+01:00",
|
||||
"e7ea9cd05dc11d5503115522b87cac7704eafc0e"
|
||||
],
|
||||
"refs/remotes/origin/master": [
|
||||
"2019-06-30T23:01:34+01:00",
|
||||
"e7ea9cd05dc11d5503115522b87cac7704eafc0e"
|
||||
]
|
||||
},
|
||||
"A2plus": {
|
||||
"refs/remotes/origin/HEAD": [
|
||||
"2022-10-02T22:20:41+02:00",
|
||||
"3392d72ded45918ea28cd46ed4de778571488639"
|
||||
],
|
||||
"refs/remotes/origin/devel": [
|
||||
"2022-01-27T19:10:27+01:00",
|
||||
"dcc6193f36d4c7c8c43c4f3384e2bc03d27debd3"
|
||||
],
|
||||
"refs/remotes/origin/master": [
|
||||
"2022-10-02T22:20:41+02:00",
|
||||
"3392d72ded45918ea28cd46ed4de778571488639"
|
||||
],
|
||||
"refs/tags/V0.1.4.1": [
|
||||
"2018-10-28T16:38:46+01:00",
|
||||
"6f6ff0a892efca602ec916faac11b041be792fe2"
|
||||
],
|
||||
"refs/tags/V0.1.5": [
|
||||
"2018-11-01T19:03:15+01:00",
|
||||
"db6cdde71d4a2e9a53f2b14694e2ba2b42ac58c0"
|
||||
],
|
||||
"refs/tags/V0.1.6": [
|
||||
"2018-11-11T16:04:51+01:00",
|
||||
"46d4039c1fd08b3e5604d4fad3e955bca5855338"
|
||||
],
|
||||
"refs/tags/V0.4.6": [
|
||||
"2019-03-19T15:19:32+01:00",
|
||||
"42d5ed3846f24c7a7f0f6db655513e9038c3f651"
|
||||
]
|
||||
},
|
||||
"AirPlaneDesign": {
|
||||
"refs/remotes/origin/HEAD": [
|
||||
"2022-07-03T19:21:44+02:00",
|
||||
"34a5c5a827a378d4b6c9c16c54a1f2238b8e3b6b"
|
||||
],
|
||||
"refs/remotes/origin/dev-v0.4": [
|
||||
"2021-01-23T16:15:58+01:00",
|
||||
"f21bb8fca55b7d34fe920f0b823a7722ff977e43"
|
||||
],
|
||||
"refs/remotes/origin/dev-v03bis": [
|
||||
"2021-08-03T18:28:22+02:00",
|
||||
"5e2b4ebafaaed6ca87b99f480ae2fecd3722a83b"
|
||||
],
|
||||
"refs/remotes/origin/master": [
|
||||
"2022-07-03T19:21:44+02:00",
|
||||
"34a5c5a827a378d4b6c9c16c54a1f2238b8e3b6b"
|
||||
],
|
||||
"refs/remotes/origin/refactoring": [
|
||||
"2021-10-18T18:41:10+02:00",
|
||||
"2f22aa7faa049b0e8ec7dd43a39554b597c7e772"
|
||||
],
|
||||
"refs/tags/V0.1": [
|
||||
"2018-08-18T20:27:30+02:00",
|
||||
"c5bf85c552b31c0abdca7cadf428099c0b38e97e"
|
||||
],
|
||||
"refs/tags/V0.2": [
|
||||
"2019-07-30T08:36:55+02:00",
|
||||
"39a7c90ac53148c3317d50e07584c96f54db549e"
|
||||
],
|
||||
"refs/tags/V0.3": [
|
||||
"2019-10-25T08:44:43+02:00",
|
||||
"8e11fb6a5c33479570e0f572e6319f220e3cf6fd"
|
||||
]
|
||||
},
|
||||
"Curves": {
|
||||
"refs/remotes/origin/HEAD": [
|
||||
"2022-10-06T19:18:14+02:00",
|
||||
"b43970d71ccfed6a85d6e547993863520db9920c"
|
||||
],
|
||||
"refs/remotes/origin/blendcurve": [
|
||||
"2020-02-26T18:55:13+01:00",
|
||||
"41dff49edfadc7b3085d4afff3c32b598fcd3ed0"
|
||||
],
|
||||
"refs/remotes/origin/blending": [
|
||||
"2022-06-24T17:52:22+02:00",
|
||||
"687b38faad85187e486fe68ccf072adb7d06be11"
|
||||
],
|
||||
"refs/remotes/origin/decimate_edges": [
|
||||
"2021-05-18T18:40:44+02:00",
|
||||
"b299597f3817cfb29fd45a8c5710706ae01261fe"
|
||||
],
|
||||
"refs/remotes/origin/facemap": [
|
||||
"2021-12-21T19:06:20+01:00",
|
||||
"7f315ab0e72d61fb6d04a2da631c66f8f3a30c82"
|
||||
],
|
||||
"refs/remotes/origin/flatten": [
|
||||
"2022-05-09T15:18:05+02:00",
|
||||
"436b135ff1c2d93686911c2e81ef4fa379e5b001"
|
||||
],
|
||||
"refs/remotes/origin/master": [
|
||||
"2022-10-06T19:18:14+02:00",
|
||||
"b43970d71ccfed6a85d6e547993863520db9920c"
|
||||
],
|
||||
"refs/remotes/origin/reflect2": [
|
||||
"2021-04-10T13:42:07+02:00",
|
||||
"6245d86e18f0e5c0a1e724ac614a369da18590e3"
|
||||
],
|
||||
"refs/remotes/origin/rotsweep": [
|
||||
"2022-10-05T17:50:39+02:00",
|
||||
"99b5e2619481257636d864921d22f60ca0af24d8"
|
||||
],
|
||||
"refs/remotes/origin/seamcheck": [
|
||||
"2022-03-18T17:53:05+01:00",
|
||||
"2703f93074a94ecdf09c4bb445bef347c206f00f"
|
||||
],
|
||||
"refs/remotes/origin/solid": [
|
||||
"2021-03-01T22:16:08+01:00",
|
||||
"4f4cc7ec6672b48660601b566777366fd0685a5d"
|
||||
],
|
||||
"refs/tags/v0.1": [
|
||||
"2019-02-17T08:57:09+01:00",
|
||||
"74ea77f091cf3a62e1dee3b64b49e9ab9fcdb560"
|
||||
],
|
||||
"refs/tags/v0.2": [
|
||||
"2020-06-16T15:02:23+02:00",
|
||||
"521037588be1aa28320ce6c9b9f77b40f4c8aeb6"
|
||||
],
|
||||
"refs/tags/v0.3": [
|
||||
"2021-01-08T19:06:59+01:00",
|
||||
"a1fa4857b1de95da1062acc63919acdee59f045c"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Bundle</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.0</version>
|
||||
<date>2025-02-22</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
|
||||
<content>
|
||||
<bundle>
|
||||
<name>A bunch of great addons you should install</name>
|
||||
<depend type="addon">TestAddon1</depend>
|
||||
<depend type="addon">TestAddon2</depend>
|
||||
<depend type="addon">TestAddon3</depend>
|
||||
<depend type="addon">TestAddon4</depend>
|
||||
<depend type="addon">TestAddon5</depend>
|
||||
</bundle>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Combination Test</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
<tag>Tag0</tag>
|
||||
<tag>Tag1</tag>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<classname>MyFirstWorkbench</classname>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
</workbench>
|
||||
<macro>
|
||||
<file>MyMacro.FCStd</file>
|
||||
</macro>
|
||||
<preferencepack>
|
||||
<name>MyFirstPack</name>
|
||||
</preferencepack>
|
||||
<bundle>
|
||||
<name>A bundle that bundles nothing</name>
|
||||
</bundle>
|
||||
<other>
|
||||
<name>Mysterious Object</name>
|
||||
</other>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Workbenches</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<classname>MyFirstWorkbench</classname>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
<depend>BIM</depend>
|
||||
<depend>Assembly</depend>
|
||||
<depend>DraftWB</depend>
|
||||
<depend>FEM WB</depend>
|
||||
<depend>MeshWorkbench</depend>
|
||||
<depend>OpenSCAD Workbench</depend>
|
||||
<depend>Part WORKBENCH</depend>
|
||||
<depend>PartDesign WB</depend>
|
||||
<depend>CAM</depend>
|
||||
<depend>Plot</depend>
|
||||
<depend>POINTS</depend>
|
||||
<depend>ROBOTWB</depend>
|
||||
<depend>Sketcher workbench</depend>
|
||||
<depend>Spreadsheet</depend>
|
||||
<depend>TechDraw</depend>
|
||||
</workbench>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,23 +0,0 @@
|
||||
[submodule "3DfindIT"]
|
||||
path = 3DfindIT
|
||||
url = https://github.com/cadenasgmbh/3dfindit-freecad-integration
|
||||
[submodule "A2plus"]
|
||||
path = A2plus
|
||||
url = https://github.com/kbwbe/A2plus
|
||||
[submodule "Behave-Dark-Colors"]
|
||||
path = Behave-Dark-Colors
|
||||
url = https://github.com/Chrismettal/FreeCAD-Behave-Dark-Preference-Pack
|
||||
branch = main
|
||||
[submodule "Beltrami"]
|
||||
path = Beltrami
|
||||
url = https://github.com/Simturb/Beltrami
|
||||
branch = main
|
||||
[submodule "CurvedShapes"]
|
||||
path = CurvedShapes
|
||||
url = https://github.com/chbergmann/CurvedShapesWorkbench.git
|
||||
[submodule "Curves"]
|
||||
path = Curves
|
||||
url = https://github.com/tomate44/CurvesWB.git
|
||||
[submodule "Defeaturing"]
|
||||
path = Defeaturing
|
||||
url = https://github.com/easyw/Defeaturing_WB.git
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Workbench</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
<tag>Tag0</tag>
|
||||
<tag>Tag1</tag>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<classname>MyWorkbench</classname>
|
||||
<subdirectory>./</subdirectory>
|
||||
<tag>TagA</tag>
|
||||
<tag>TagB</tag>
|
||||
<tag>TagC</tag>
|
||||
</workbench>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1 +0,0 @@
|
||||
67b372f9a5ac11e5377a4075537f31dfebf61753 *icon_cache.zip
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Macros</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
|
||||
<content>
|
||||
<macro>
|
||||
<file>MyMacro.FCStd</file>
|
||||
</macro>
|
||||
<macro>
|
||||
<file>MyOtherMacro.FCStd</file>
|
||||
</macro>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,37 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of the FreeCAD CAx development system. *
|
||||
# * *
|
||||
# * This library is free software; you can redistribute it and/or *
|
||||
# * modify it under the terms of the GNU Lesser General Public *
|
||||
# * License as published by the Free Software Foundation; either *
|
||||
# * version 2.1 of the License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * This library is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with this library; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
# * 02110-1301 USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
__Title__ = "TITLE"
|
||||
__Author__ = "AUTHOR"
|
||||
__Date__ = "DATE"
|
||||
__Version__ = "VERSION"
|
||||
__Comment__ = "COMMENT"
|
||||
__Web__ = "WEB"
|
||||
__Wiki__ = "WIKI"
|
||||
__Icon__ = "ICON"
|
||||
__Help__ = "HELP"
|
||||
__Status__ = "STATUS"
|
||||
__Requires__ = "REQUIRES"
|
||||
__Communication__ = "COMMUNICATION"
|
||||
__Files__ = "FILES"
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of the FreeCAD CAx development system. *
|
||||
# * *
|
||||
# * This library is free software; you can redistribute it and/or *
|
||||
# * modify it under the terms of the GNU Lesser General Public *
|
||||
# * License as published by the Free Software Foundation; either *
|
||||
# * version 2.1 of the License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * This library is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with this library; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
# * 02110-1301 USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
# This file contains no metadata
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Other</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.0</version>
|
||||
<date>2025-02-22</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
|
||||
<content>
|
||||
<other>
|
||||
<name>A thing that's not a workbench, macro, preference pack, or bundle</name>
|
||||
</other>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Preference Packs</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
|
||||
<content>
|
||||
<preferencepack>
|
||||
<name>MyFirstPack</name>
|
||||
</preferencepack>
|
||||
<preferencepack>
|
||||
<name>MySecondPack</name>
|
||||
</preferencepack>
|
||||
<preferencepack>
|
||||
<name>MyThirdPack</name>
|
||||
</preferencepack>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Workbenches</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
|
||||
<content>
|
||||
<macro>
|
||||
<name>MacroA</name>
|
||||
<tag>TagA</tag>
|
||||
<freecadmin>0.1</freecadmin>
|
||||
<freecadmax>0.10</freecadmax>
|
||||
</macro>
|
||||
<macro>
|
||||
<name>MacroB</name>
|
||||
<tag>TagB</tag>
|
||||
<freecadmin>0.20</freecadmin>
|
||||
<freecadmax>9999.98</freecadmax>
|
||||
</macro>
|
||||
<macro>
|
||||
<name>MacroC</name>
|
||||
<tag>TagC</tag>
|
||||
<freecadmin>9999.99</freecadmin>
|
||||
<freecadmax>99999.99</freecadmax>
|
||||
</macro>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>Test Workbenches</name>
|
||||
<description>A package.xml file for unit testing.</description>
|
||||
<version>1.0.1</version>
|
||||
<date>2022-01-07</date>
|
||||
<maintainer email="developer@freecad.org">FreeCAD Developer</maintainer>
|
||||
<license file="LICENSE">LGPL-2.1</license>
|
||||
<url type="repository" branch="main">https://github.com/chennes/FreeCAD-Package</url>
|
||||
<url type="readme">https://github.com/chennes/FreeCAD-Package/blob/main/README.md</url>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<classname>MyFirstWorkbench</classname>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
</workbench>
|
||||
<workbench>
|
||||
<classname>MySecondWorkbench</classname>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
</workbench>
|
||||
<workbench>
|
||||
<classname>MyThirdWorkbench</classname>
|
||||
<icon>Resources/icons/PackageIcon.svg</icon>
|
||||
</workbench>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
@@ -1,157 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from PySide import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
try:
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
|
||||
sys.path.append("../../") # For running in standalone mode during testing
|
||||
|
||||
from AddonManagerTest.app.mocks import SignalCatcher
|
||||
|
||||
|
||||
class DialogInteractor(QtCore.QObject):
|
||||
"""Takes the title of the dialog and a callable. The callable is passed the widget
|
||||
we found and can do whatever it wants to it. Whatever it does should eventually
|
||||
close the dialog, however."""
|
||||
|
||||
def __init__(self, dialog_to_watch_for, interaction):
|
||||
super().__init__()
|
||||
|
||||
# Status variables for tests to check:
|
||||
self.dialog_found = False
|
||||
self.has_run = False
|
||||
self.button_found = False
|
||||
self.interaction = interaction
|
||||
|
||||
self.dialog_to_watch_for = dialog_to_watch_for
|
||||
|
||||
self.execution_counter = 0
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.run)
|
||||
self.timer.start(
|
||||
1
|
||||
) # At 10 this occasionally left open dialogs; less than 1 produced failed tests
|
||||
|
||||
def run(self):
|
||||
widget = QtWidgets.QApplication.activeModalWidget()
|
||||
if widget and self._dialog_matches(widget):
|
||||
# Found the dialog we are looking for: now try to run the interaction
|
||||
if self.interaction is not None and callable(self.interaction):
|
||||
self.interaction(widget)
|
||||
self.dialog_found = True
|
||||
self.timer.stop()
|
||||
|
||||
self.has_run = True
|
||||
self.execution_counter += 1
|
||||
if self.execution_counter > 100:
|
||||
print("Stopped timer after 100 iterations")
|
||||
self.timer.stop()
|
||||
|
||||
def _dialog_matches(self, widget) -> bool:
|
||||
# Is this the widget we are looking for? Only applies on Linux and Windows: macOS
|
||||
# doesn't set the title of a modal dialog:
|
||||
os = QtCore.QSysInfo.productType() # Qt5 gives "osx", Qt6 gives "macos"
|
||||
if os in ["osx", "macos"] or (
|
||||
hasattr(widget, "windowTitle")
|
||||
and callable(widget.windowTitle)
|
||||
and widget.windowTitle() == self.dialog_to_watch_for
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DialogWatcher(DialogInteractor):
|
||||
"""Examine the running GUI and look for a modal dialog with a given title, containing a button
|
||||
with a role. Click that button, which is expected to close the dialog. Generally run on
|
||||
a one-shot QTimer to allow the dialog time to open up. If the specified dialog is found, but
|
||||
it does not contain the expected button, button_found will be false, and the dialog will be
|
||||
closed with a reject() slot."""
|
||||
|
||||
def __init__(self, dialog_to_watch_for, button=QtWidgets.QDialogButtonBox.NoButton):
|
||||
super().__init__(dialog_to_watch_for, self.click_button)
|
||||
if button != QtWidgets.QDialogButtonBox.NoButton:
|
||||
self.button = button
|
||||
else:
|
||||
self.button = QtWidgets.QDialogButtonBox.Cancel
|
||||
|
||||
def click_button(self, widget):
|
||||
button_boxes = widget.findChildren(QtWidgets.QDialogButtonBox)
|
||||
if len(button_boxes) == 1: # There should be one, and only one
|
||||
button_to_click = button_boxes[0].button(self.button)
|
||||
if button_to_click:
|
||||
self.button_found = True
|
||||
button_to_click.click()
|
||||
else:
|
||||
widget.reject()
|
||||
else:
|
||||
widget.reject()
|
||||
|
||||
|
||||
class FakeWorker:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.should_continue = True
|
||||
|
||||
def work(self):
|
||||
while self.should_continue:
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
|
||||
def stop(self):
|
||||
self.should_continue = False
|
||||
|
||||
|
||||
class MockThread:
|
||||
def wait(self):
|
||||
pass
|
||||
|
||||
def isRunning(self):
|
||||
return False
|
||||
|
||||
|
||||
class AsynchronousMonitor:
|
||||
"""Watch for a signal to be emitted for at most some given number of milliseconds"""
|
||||
|
||||
def __init__(self, signal):
|
||||
self.signal = signal
|
||||
self.signal_catcher = SignalCatcher()
|
||||
self.signal.connect(self.signal_catcher.catch_signal)
|
||||
self.kill_timer = QtCore.QTimer()
|
||||
self.kill_timer.setSingleShot(True)
|
||||
self.kill_timer.timeout.connect(self.signal_catcher.die)
|
||||
|
||||
def wait_for_at_most(self, max_wait_millis) -> None:
|
||||
self.kill_timer.setInterval(max_wait_millis)
|
||||
self.kill_timer.start()
|
||||
while not self.signal_catcher.caught and not self.signal_catcher.killed:
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 10)
|
||||
self.kill_timer.stop()
|
||||
|
||||
def good(self) -> bool:
|
||||
return self.signal_catcher.caught and not self.signal_catcher.killed
|
||||
@@ -1,234 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 The FreeCAD Project Association AISBL *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Test the Change Branch GUI code"""
|
||||
|
||||
# pylint: disable=wrong-import-position, deprecated-module, too-many-return-statements
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch, Mock, MagicMock
|
||||
|
||||
# So that when run standalone, the Addon Manager classes imported below are available
|
||||
sys.path.append("../..")
|
||||
|
||||
from AddonManagerTest.gui.gui_mocks import DialogWatcher, AsynchronousMonitor
|
||||
|
||||
from change_branch import ChangeBranchDialog
|
||||
|
||||
from addonmanager_freecad_interface import translate
|
||||
from addonmanager_git import GitFailed
|
||||
|
||||
try:
|
||||
from PySide import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
try:
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
|
||||
|
||||
class MockFilter(QtCore.QSortFilterProxyModel):
|
||||
"""Replaces a filter with a non-filter that simply always returns whatever it's given"""
|
||||
|
||||
def mapToSource(self, something):
|
||||
return something
|
||||
|
||||
|
||||
class MockChangeBranchDialogModel(QtCore.QAbstractTableModel):
|
||||
"""Replace a data-connected model with a static one for testing"""
|
||||
|
||||
branches = [
|
||||
{"ref_name": "ref1", "upstream": "us1"},
|
||||
{"ref_name": "ref2", "upstream": "us2"},
|
||||
{"ref_name": "ref3", "upstream": "us3"},
|
||||
]
|
||||
current_branch = "ref1"
|
||||
DataSortRole = QtCore.Qt.UserRole
|
||||
RefAccessRole = QtCore.Qt.UserRole + 1
|
||||
|
||||
def __init__(self, _: str, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
"""Number of rows: should always return 3"""
|
||||
if parent.isValid():
|
||||
return 0
|
||||
return len(self.branches)
|
||||
|
||||
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
"""Number of columns (identical to non-mocked version)"""
|
||||
if parent.isValid():
|
||||
return 0
|
||||
return 3 # Local name, remote name, date
|
||||
|
||||
def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
|
||||
"""Mock returns static untranslated strings for DisplayRole, no tooltips at all, and
|
||||
otherwise matches the non-mock version"""
|
||||
if not index.isValid():
|
||||
return None
|
||||
row = index.row()
|
||||
column = index.column()
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if column == 2:
|
||||
return "date"
|
||||
if column == 0:
|
||||
return "ref_name"
|
||||
if column == 1:
|
||||
return "upstream"
|
||||
return None
|
||||
if role == MockChangeBranchDialogModel.DataSortRole:
|
||||
return None
|
||||
if role == MockChangeBranchDialogModel.RefAccessRole:
|
||||
return self.branches[row]
|
||||
return None
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section: int,
|
||||
orientation: QtCore.Qt.Orientation,
|
||||
role: int = QtCore.Qt.DisplayRole,
|
||||
):
|
||||
"""Mock returns untranslated strings for DisplayRole, and no tooltips at all"""
|
||||
if orientation == QtCore.Qt.Vertical:
|
||||
return None
|
||||
if role != QtCore.Qt.DisplayRole:
|
||||
return None
|
||||
if section == 0:
|
||||
return "Local"
|
||||
if section == 1:
|
||||
return "Remote tracking"
|
||||
if section == 2:
|
||||
return "Last Updated"
|
||||
return None
|
||||
|
||||
def currentBranch(self) -> str:
|
||||
"""Mock returns a static string stored in the class: that string could be modified to
|
||||
return something else by tests that require it."""
|
||||
return self.current_branch
|
||||
|
||||
|
||||
class TestChangeBranchGui(unittest.TestCase):
|
||||
"""Tests for the ChangeBranch GUI code"""
|
||||
|
||||
MODULE = "test_change_branch" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
|
||||
@patch("change_branch.initialize_git", new=Mock(return_value=None))
|
||||
def test_no_git(self):
|
||||
"""If git is not present, a dialog saying so is presented"""
|
||||
# Arrange
|
||||
gui = ChangeBranchDialog("/some/path")
|
||||
ref = {"ref_name": "foo/bar", "upstream": "us1"}
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot find git"),
|
||||
QtWidgets.QDialogButtonBox.Ok,
|
||||
)
|
||||
|
||||
# Act
|
||||
gui.change_branch("/foo/bar/baz", ref)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
|
||||
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
|
||||
@patch("change_branch.initialize_git")
|
||||
def test_git_failed(self, init_git: MagicMock):
|
||||
"""If git fails when attempting to change branches, a dialog saying so is presented"""
|
||||
# Arrange
|
||||
git_manager = MagicMock()
|
||||
git_manager.checkout = MagicMock()
|
||||
git_manager.checkout.side_effect = GitFailed()
|
||||
init_git.return_value = git_manager
|
||||
gui = ChangeBranchDialog("/some/path")
|
||||
ref = {"ref_name": "foo/bar", "upstream": "us1"}
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "git operation failed"),
|
||||
QtWidgets.QDialogButtonBox.Ok,
|
||||
)
|
||||
|
||||
# Act
|
||||
gui.change_branch("/foo/bar/baz", ref)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
|
||||
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
|
||||
@patch("change_branch.initialize_git", new=MagicMock)
|
||||
def test_branch_change_succeeded(self):
|
||||
"""If nothing gets thrown, then the process is assumed to have worked, and the appropriate
|
||||
signal is emitted."""
|
||||
|
||||
# Arrange
|
||||
gui = ChangeBranchDialog("/some/path")
|
||||
ref = {"ref_name": "foo/bar", "upstream": "us1"}
|
||||
monitor = AsynchronousMonitor(gui.branch_changed)
|
||||
|
||||
# Act
|
||||
gui.change_branch("/foo/bar/baz", ref)
|
||||
|
||||
# Assert
|
||||
monitor.wait_for_at_most(10) # Should be effectively instantaneous
|
||||
self.assertTrue(monitor.good())
|
||||
|
||||
@patch("change_branch.ChangeBranchDialogFilter", new=MockFilter)
|
||||
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
|
||||
@patch("change_branch.initialize_git", new=MagicMock)
|
||||
def test_warning_is_shown_when_dialog_is_accepted(self):
|
||||
"""If the dialog is accepted (e.g. a branch change is requested) then a warning dialog is
|
||||
displayed, and gives the opportunity to cancel. If cancelled, no signal is emitted."""
|
||||
# Arrange
|
||||
gui = ChangeBranchDialog("/some/path")
|
||||
gui.ui.exec = MagicMock()
|
||||
gui.ui.exec.return_value = QtWidgets.QDialog.Accepted
|
||||
gui.ui.tableView.selectedIndexes = MagicMock()
|
||||
gui.ui.tableView.selectedIndexes.return_value = [MagicMock()]
|
||||
gui.ui.tableView.selectedIndexes.return_value[0].isValid = MagicMock()
|
||||
gui.ui.tableView.selectedIndexes.return_value[0].isValid.return_value = True
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "DANGER: Developer feature"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
monitor = AsynchronousMonitor(gui.branch_changed)
|
||||
|
||||
# Act
|
||||
gui.exec()
|
||||
|
||||
# Assert
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertFalse(monitor.good()) # The watcher cancelled the op, so no signal is emitted
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
QtCore.QTimer.singleShot(0, unittest.main)
|
||||
if hasattr(app, "exec"):
|
||||
app.exec() # PySide6
|
||||
else:
|
||||
app.exec_() # PySide2
|
||||
@@ -1,33 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
|
||||
class TestGui(unittest.TestCase):
|
||||
|
||||
MODULE = "test_gui" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
@@ -1,650 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
|
||||
|
||||
from AddonManagerTest.gui.gui_mocks import DialogWatcher, DialogInteractor
|
||||
from AddonManagerTest.app.mocks import MockAddon
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TestInstallerGui(unittest.TestCase):
|
||||
|
||||
MODULE = "test_installer_gui" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.addon_to_install = MockAddon()
|
||||
self.installer_gui = AddonInstallerGUI(self.addon_to_install)
|
||||
self.finalized_thread = False
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_success_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to an OK click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Success"),
|
||||
QtWidgets.QDialogButtonBox.Ok,
|
||||
)
|
||||
self.installer_gui._installation_succeeded()
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_failure_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a Cancel click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Installation Failed"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.installer_gui._installation_failed(
|
||||
self.addon_to_install, "Test of installation failure"
|
||||
)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_no_python_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot execute Python"),
|
||||
QtWidgets.QDialogButtonBox.No,
|
||||
)
|
||||
self.installer_gui._report_no_python_exe()
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_no_pip_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot execute pip"),
|
||||
QtWidgets.QDialogButtonBox.No,
|
||||
)
|
||||
self.installer_gui._report_no_pip("pip not actually run, this was a test")
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_dependency_failure_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Package installation failed"),
|
||||
QtWidgets.QDialogButtonBox.No,
|
||||
)
|
||||
self.installer_gui._report_dependency_failure(
|
||||
"Unit test", "Nothing really failed, this is a test of the dialog box"
|
||||
)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_install(self):
|
||||
# Run the installation code and make sure it puts the directory in place
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.installer_gui.installer.installation_path = temp_dir
|
||||
self.installer_gui.install() # This does not block
|
||||
self.installer_gui.installer.success.disconnect(
|
||||
self.installer_gui._installation_succeeded
|
||||
)
|
||||
self.installer_gui.installer.failure.disconnect(self.installer_gui._installation_failed)
|
||||
while not self.installer_gui.worker_thread.isFinished():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, "MockAddon")),
|
||||
"Installed directory not found",
|
||||
)
|
||||
|
||||
def test_handle_disallowed_python(self):
|
||||
disallowed_packages = ["disallowed_package_name"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.installer_gui._handle_disallowed_python(disallowed_packages)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_handle_disallowed_python_long_list(self):
|
||||
"""A separate test for when there are MANY packages, which takes a separate code path."""
|
||||
disallowed_packages = []
|
||||
for i in range(50):
|
||||
disallowed_packages.append(f"disallowed_package_name_{i}")
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.installer_gui._handle_disallowed_python(disallowed_packages)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_report_missing_workbenches_single(self):
|
||||
"""Test only missing one workbench"""
|
||||
wbs = ["OneMissingWorkbench"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.installer_gui._report_missing_workbenches(wbs)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_report_missing_workbenches_multiple(self):
|
||||
"""Test only missing one workbench"""
|
||||
wbs = ["FirstMissingWorkbench", "SecondMissingWorkbench"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.installer_gui._report_missing_workbenches(wbs)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_resolve_dependencies_then_install(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.external_addons = ["addon_1", "addon_2"]
|
||||
self.python_requires = ["py_req_1", "py_req_2"]
|
||||
self.python_optional = ["py_opt_1", "py_opt_2"]
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("DependencyResolutionDialog", "Resolve Dependencies"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.installer_gui._resolve_dependencies_then_install(missing)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_check_python_version_bad(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_min_version = {"major": 3, "minor": 9999}
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Incompatible Python version"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
stop_installing = self.installer_gui._check_python_version(missing)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
self.assertTrue(stop_installing, "Failed to halt installation on bad Python version")
|
||||
|
||||
def test_check_python_version_good(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_min_version = {"major": 3, "minor": 0}
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
stop_installing = self.installer_gui._check_python_version(missing)
|
||||
self.assertFalse(stop_installing, "Failed to continue installation on good Python version")
|
||||
|
||||
def test_clean_up_optional(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_optional = [
|
||||
"allowed_packages_1",
|
||||
"allowed_packages_2",
|
||||
"disallowed_package",
|
||||
]
|
||||
|
||||
allowed_packages = ["allowed_packages_1", "allowed_packages_2"]
|
||||
missing = MissingDependenciesMock()
|
||||
self.installer_gui.installer.allowed_packages = set(allowed_packages)
|
||||
self.installer_gui._clean_up_optional(missing)
|
||||
self.assertTrue("allowed_packages_1" in missing.python_optional)
|
||||
self.assertTrue("allowed_packages_2" in missing.python_optional)
|
||||
self.assertFalse("disallowed_package" in missing.python_optional)
|
||||
|
||||
def intercept_run_dependency_installer(self, addons, python_requires, python_optional):
|
||||
self.assertEqual(python_requires, ["py_req_1", "py_req_2"])
|
||||
self.assertEqual(python_optional, ["py_opt_1", "py_opt_2"])
|
||||
self.assertEqual(addons[0].name, "addon_1")
|
||||
self.assertEqual(addons[1].name, "addon_2")
|
||||
|
||||
def test_dependency_dialog_yes_clicked(self):
|
||||
class DialogMock:
|
||||
class ListWidgetMock:
|
||||
class ListWidgetItemMock:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def text(self):
|
||||
return self.name
|
||||
|
||||
def checkState(self):
|
||||
return QtCore.Qt.Checked
|
||||
|
||||
def __init__(self, items):
|
||||
self.list = []
|
||||
for item in items:
|
||||
self.list.append(DialogMock.ListWidgetMock.ListWidgetItemMock(item))
|
||||
|
||||
def count(self):
|
||||
return len(self.list)
|
||||
|
||||
def item(self, i):
|
||||
return self.list[i]
|
||||
|
||||
def __init__(self):
|
||||
self.listWidgetAddons = DialogMock.ListWidgetMock(["addon_1", "addon_2"])
|
||||
self.listWidgetPythonRequired = DialogMock.ListWidgetMock(["py_req_1", "py_req_2"])
|
||||
self.listWidgetPythonOptional = DialogMock.ListWidgetMock(["py_opt_1", "py_opt_2"])
|
||||
|
||||
class AddonMock:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
self.installer_gui.dependency_dialog = DialogMock()
|
||||
self.installer_gui.addons = [AddonMock("addon_1"), AddonMock("addon_2")]
|
||||
self.installer_gui._run_dependency_installer = self.intercept_run_dependency_installer
|
||||
self.installer_gui._dependency_dialog_yes_clicked()
|
||||
|
||||
|
||||
class TestMacroInstallerGui(unittest.TestCase):
|
||||
class MockMacroAddon:
|
||||
class MockMacro:
|
||||
def __init__(self):
|
||||
self.install_called = False
|
||||
self.install_result = (
|
||||
True # External code can change to False to test failed install
|
||||
)
|
||||
self.name = "MockMacro"
|
||||
self.filename = "mock_macro_no_real_file.FCMacro"
|
||||
self.comment = "This is a mock macro for unit testing"
|
||||
self.icon = None
|
||||
self.xpm = None
|
||||
|
||||
def install(self):
|
||||
self.install_called = True
|
||||
return self.install_result
|
||||
|
||||
def __init__(self):
|
||||
self.macro = TestMacroInstallerGui.MockMacroAddon.MockMacro()
|
||||
self.name = self.macro.name
|
||||
self.display_name = self.macro.name
|
||||
|
||||
class MockParameter:
|
||||
"""Mock the parameter group to allow simplified behavior and introspection."""
|
||||
|
||||
def __init__(self):
|
||||
self.params = {}
|
||||
self.groups = {}
|
||||
self.accessed_parameters = {} # Dict is param name: default value
|
||||
|
||||
types = ["Bool", "String", "Int", "UInt", "Float"]
|
||||
for t in types:
|
||||
setattr(self, f"Get{t}", self.get)
|
||||
setattr(self, f"Set{t}", self.set)
|
||||
setattr(self, f"Rem{t}", self.rem)
|
||||
|
||||
def get(self, p, default=None):
|
||||
self.accessed_parameters[p] = default
|
||||
if p in self.params:
|
||||
return self.params[p]
|
||||
else:
|
||||
return default
|
||||
|
||||
def set(self, p, value):
|
||||
self.params[p] = value
|
||||
|
||||
def rem(self, p):
|
||||
if p in self.params:
|
||||
self.params.erase(p)
|
||||
|
||||
def GetGroup(self, name):
|
||||
if name not in self.groups:
|
||||
self.groups[name] = TestMacroInstallerGui.MockParameter()
|
||||
return self.groups[name]
|
||||
|
||||
def GetGroups(self):
|
||||
return self.groups.keys()
|
||||
|
||||
class ToolbarIntercepter:
|
||||
def __init__(self):
|
||||
self.ask_for_toolbar_called = False
|
||||
self.install_macro_to_toolbar_called = False
|
||||
self.tb = None
|
||||
self.custom_group = TestMacroInstallerGui.MockParameter()
|
||||
self.custom_group.set("Name", "MockCustomToolbar")
|
||||
|
||||
def _ask_for_toolbar(self, _):
|
||||
self.ask_for_toolbar_called = True
|
||||
return self.custom_group
|
||||
|
||||
def _install_macro_to_toolbar(self, tb):
|
||||
self.install_macro_to_toolbar_called = True
|
||||
self.tb = tb
|
||||
|
||||
class InstallerInterceptor:
|
||||
def __init__(self):
|
||||
self.ccc_called = False
|
||||
|
||||
def _create_custom_command(
|
||||
self,
|
||||
toolbar,
|
||||
filename,
|
||||
menuText,
|
||||
tooltipText,
|
||||
whatsThisText,
|
||||
statustipText,
|
||||
pixmapText,
|
||||
):
|
||||
self.ccc_called = True
|
||||
self.toolbar = toolbar
|
||||
self.filename = filename
|
||||
self.menuText = menuText
|
||||
self.tooltipText = tooltipText
|
||||
self.whatsThisText = whatsThisText
|
||||
self.statustipText = statustipText
|
||||
self.pixmapText = pixmapText
|
||||
|
||||
def setUp(self):
|
||||
self.mock_macro = TestMacroInstallerGui.MockMacroAddon()
|
||||
self.installer = MacroInstallerGUI(self.mock_macro)
|
||||
self.installer.addon_params = TestMacroInstallerGui.MockParameter()
|
||||
self.installer.toolbar_params = TestMacroInstallerGui.MockParameter()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_class_is_initialized(self):
|
||||
"""Connecting to a signal does not throw"""
|
||||
self.installer.finished.connect(lambda: None)
|
||||
|
||||
def test_ask_for_toolbar_no_dialog_default_exists(self):
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", False)
|
||||
self.installer.addon_params.set("CustomToolbarName", "UnitTestCustomToolbar")
|
||||
utct = self.installer.toolbar_params.GetGroup("UnitTestCustomToolbar")
|
||||
utct.set("Name", "UnitTestCustomToolbar")
|
||||
utct.set("Active", True)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UnitTestCustomToolbar")
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_cancelled(self):
|
||||
|
||||
# First test: the user cancels the dialog
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", True)
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("select_toolbar_dialog", "Select Toolbar"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_defaults(self):
|
||||
|
||||
# Second test: the user leaves the dialog at all default values, so:
|
||||
# - The checkbox "Ask every time" is unchecked
|
||||
# - The selected toolbar option is "Create new toolbar", which triggers a search for
|
||||
# a new custom toolbar name by calling _create_new_custom_toolbar, which we mock.
|
||||
fake_custom_toolbar_group = TestMacroInstallerGui.MockParameter()
|
||||
fake_custom_toolbar_group.set("Name", "UnitTestCustomToolbar")
|
||||
self.installer._create_new_custom_toolbar = lambda: fake_custom_toolbar_group
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("select_toolbar_dialog", "Select Toolbar"),
|
||||
QtWidgets.QDialogButtonBox.Ok,
|
||||
)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UnitTestCustomToolbar")
|
||||
self.assertIn("alwaysAskForToolbar", self.installer.addon_params.params)
|
||||
self.assertFalse(self.installer.addon_params.get("alwaysAskForToolbar", True))
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_selection(self):
|
||||
|
||||
# Third test: the user selects a custom toolbar in the dialog, and checks the box to always
|
||||
# ask.
|
||||
dialog_interactor = DialogInteractor(
|
||||
translate("select_toolbar_dialog", "Select Toolbar"),
|
||||
self.interactor_selection_option_and_checkbox,
|
||||
)
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
result = self.installer._ask_for_toolbar(["UT_TB_1", "UT_TB_2", "UT_TB_3"])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UT_TB_3")
|
||||
self.assertIn("alwaysAskForToolbar", self.installer.addon_params.params)
|
||||
self.assertTrue(self.installer.addon_params.get("alwaysAskForToolbar", False))
|
||||
|
||||
def interactor_selection_option_and_checkbox(self, parent):
|
||||
|
||||
boxes = parent.findChildren(QtWidgets.QComboBox)
|
||||
self.assertEqual(len(boxes), 1) # Just to make sure...
|
||||
box = boxes[0]
|
||||
box.setCurrentIndex(box.count() - 2) # Select the last thing but one
|
||||
|
||||
checkboxes = parent.findChildren(QtWidgets.QCheckBox)
|
||||
self.assertEqual(len(checkboxes), 1) # Just to make sure...
|
||||
checkbox = checkboxes[0]
|
||||
checkbox.setChecked(True)
|
||||
|
||||
parent.accept()
|
||||
|
||||
def test_macro_button_exists_no_command(self):
|
||||
# Test 1: No command for this macro
|
||||
self.installer._find_custom_command = lambda _: None
|
||||
button_exists = self.installer._macro_button_exists()
|
||||
self.assertFalse(button_exists)
|
||||
|
||||
def test_macro_button_exists_true(self):
|
||||
# Test 2: Macro is in the list of buttons
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestCommand")
|
||||
ut_tb_1.set("UnitTestCommand", "FreeCAD") # This is what the real thing looks like...
|
||||
self.installer._find_custom_command = lambda _: "UnitTestCommand"
|
||||
self.assertTrue(self.installer._macro_button_exists())
|
||||
|
||||
def test_macro_button_exists_false(self):
|
||||
# Test 3: Macro is not in the list of buttons
|
||||
self.installer._find_custom_command = lambda _: "UnitTestCommand"
|
||||
self.assertFalse(self.installer._macro_button_exists())
|
||||
|
||||
def test_ask_to_install_toolbar_button_disabled(self):
|
||||
self.installer.addon_params.SetBool("dontShowAddMacroButtonDialog", True)
|
||||
self.installer._ask_to_install_toolbar_button()
|
||||
# This should NOT block when dontShowAddMacroButtonDialog is True
|
||||
|
||||
def test_ask_to_install_toolbar_button_enabled_no(self):
|
||||
self.installer.addon_params.SetBool("dontShowAddMacroButtonDialog", False)
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("toolbar_button", "Add button?"),
|
||||
QtWidgets.QDialogButtonBox.No,
|
||||
)
|
||||
# Note: that dialog does not use a QButtonBox, so we can really only test its
|
||||
# reject() signal, which is triggered by the DialogWatcher when it cannot find
|
||||
# the button. In this case, failure to find that button is NOT an error.
|
||||
self.installer._ask_to_install_toolbar_button() # Blocks until killed by watcher
|
||||
self.assertTrue(dialog_watcher.dialog_found)
|
||||
|
||||
def test_get_toolbar_with_name_found(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestToolbar")
|
||||
ut_tb_1.set("Name", "Unit Test Toolbar")
|
||||
ut_tb_1.set("UnitTestParam", True)
|
||||
tb = self.installer._get_toolbar_with_name("Unit Test Toolbar")
|
||||
self.assertIsNotNone(tb)
|
||||
self.assertTrue(tb.get("UnitTestParam", False))
|
||||
|
||||
def test_get_toolbar_with_name_not_found(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestToolbar")
|
||||
ut_tb_1.set("Name", "Not the Unit Test Toolbar")
|
||||
tb = self.installer._get_toolbar_with_name("Unit Test Toolbar")
|
||||
self.assertIsNone(tb)
|
||||
|
||||
def test_create_new_custom_toolbar_no_existing(self):
|
||||
tb = self.installer._create_new_custom_toolbar()
|
||||
self.assertEqual(tb.get("Name", ""), "Auto-Created Macro Toolbar")
|
||||
self.assertTrue(tb.get("Active", False), True)
|
||||
|
||||
def test_create_new_custom_toolbar_one_existing(self):
|
||||
_ = self.installer._create_new_custom_toolbar()
|
||||
tb = self.installer._create_new_custom_toolbar()
|
||||
self.assertEqual(tb.get("Name", ""), "Auto-Created Macro Toolbar (2)")
|
||||
self.assertTrue(tb.get("Active", False), True)
|
||||
|
||||
def test_check_for_toolbar_true(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.assertTrue(self.installer._check_for_toolbar("UT_TB_1"))
|
||||
|
||||
def test_check_for_toolbar_false(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.assertFalse(self.installer._check_for_toolbar("Not UT_TB_1"))
|
||||
|
||||
def test_install_toolbar_button_first_custom_toolbar(self):
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertTrue("Custom_1" in self.installer.toolbar_params.GetGroups())
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_1(self):
|
||||
# There is an existing custom toolbar, and we should use it
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_1")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "UT_TB_1")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_2(self):
|
||||
# There are multiple existing custom toolbars, and we should use one of them
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_3")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "UT_TB_3")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_3(self):
|
||||
# There are multiple existing custom toolbars, but none of them match
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_4")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertTrue(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "MockCustomToolbar")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_4(self):
|
||||
# There are multiple existing custom toolbars, one of them matches, but we have set
|
||||
# "alwaysAskForToolbar" to True
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_3")
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", True)
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertTrue(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "MockCustomToolbar")
|
||||
|
||||
def test_install_macro_to_toolbar_icon_abspath(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.NamedTemporaryFile() as ntf:
|
||||
self.mock_macro.macro.icon = ntf.name
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, ntf.name)
|
||||
|
||||
def test_install_macro_to_toolbar_icon_relpath(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.mock_macro.macro.icon = "RelativeIconPath.png"
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, os.path.join(td, "RelativeIconPath.png"))
|
||||
|
||||
def test_install_macro_to_toolbar_xpm(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.mock_macro.macro.xpm = "Not really xpm data, don't try to use it!"
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, os.path.join(td, "MockMacro_icon.xpm"))
|
||||
self.assertTrue(os.path.exists(os.path.join(td, "MockMacro_icon.xpm")))
|
||||
|
||||
def test_install_macro_to_toolbar_no_icon(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertIsNone(ii.pixmapText)
|
||||
@@ -1,139 +0,0 @@
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
try:
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
except ImportError:
|
||||
try:
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
|
||||
sys.path.append(
|
||||
"../.."
|
||||
) # So that when run standalone, the Addon Manager classes imported below are available
|
||||
|
||||
from addonmanager_python_deps_gui import (
|
||||
PythonPackageManager,
|
||||
call_pip,
|
||||
PipFailed,
|
||||
python_package_updates_are_available,
|
||||
parse_pip_list_output,
|
||||
)
|
||||
from AddonManagerTest.gui.gui_mocks import DialogInteractor, DialogWatcher
|
||||
|
||||
|
||||
class TestPythonPackageManager(unittest.TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.manager = PythonPackageManager([])
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self.manager.worker_thread:
|
||||
self.manager.worker_thread.terminate()
|
||||
self.manager.worker_thread.wait()
|
||||
|
||||
@patch("addonmanager_python_deps_gui.PythonPackageManager._create_list_from_pip")
|
||||
def test_show(self, patched_create_list_from_pip):
|
||||
dialog_watcher = DialogWatcher("Manage Python Dependencies")
|
||||
self.manager.show()
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
|
||||
|
||||
class TestPythonDepsStandaloneFunctions(unittest.TestCase):
|
||||
|
||||
@patch("addonmanager_utilities.run_interruptable_subprocess")
|
||||
def test_call_pip(self, mock_run_subprocess: MagicMock):
|
||||
call_pip(["arg1", "arg2", "arg3"])
|
||||
mock_run_subprocess.assert_called()
|
||||
args = mock_run_subprocess.call_args[0][0]
|
||||
self.assertTrue("pip" in args)
|
||||
|
||||
@patch("addonmanager_python_deps_gui.get_python_exe")
|
||||
def test_call_pip_no_python(self, mock_get_python_exe: MagicMock):
|
||||
mock_get_python_exe.return_value = None
|
||||
with self.assertRaises(PipFailed):
|
||||
call_pip(["arg1", "arg2", "arg3"])
|
||||
|
||||
@patch("addonmanager_utilities.run_interruptable_subprocess")
|
||||
def test_call_pip_exception_raised(self, mock_run_subprocess: MagicMock):
|
||||
mock_run_subprocess.side_effect = subprocess.CalledProcessError(
|
||||
-1, "dummy_command", "Fake contents of stdout", "Fake contents of stderr"
|
||||
)
|
||||
with self.assertRaises(PipFailed):
|
||||
call_pip(["arg1", "arg2", "arg3"])
|
||||
|
||||
@patch("addonmanager_utilities.run_interruptable_subprocess")
|
||||
def test_call_pip_splits_results(self, mock_run_subprocess: MagicMock):
|
||||
result_mock = MagicMock()
|
||||
result_mock.stdout = "\n".join(["Value 1", "Value 2", "Value 3"])
|
||||
mock_run_subprocess.return_value = result_mock
|
||||
result = call_pip(["arg1", "arg2", "arg3"])
|
||||
self.assertEqual(len(result), 3)
|
||||
|
||||
@patch("addonmanager_python_deps_gui.call_pip")
|
||||
def test_python_package_updates_are_available(self, mock_call_pip: MagicMock):
|
||||
mock_call_pip.return_value = "Some result"
|
||||
result = python_package_updates_are_available()
|
||||
self.assertEqual(result, True)
|
||||
|
||||
@patch("addonmanager_python_deps_gui.call_pip")
|
||||
def test_python_package_updates_are_available_no_results(self, mock_call_pip: MagicMock):
|
||||
"""An empty string is an indication that no updates are available"""
|
||||
mock_call_pip.return_value = ""
|
||||
result = python_package_updates_are_available()
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@patch("addonmanager_python_deps_gui.call_pip")
|
||||
def test_python_package_updates_are_available_pip_failure(self, mock_call_pip: MagicMock):
|
||||
logging.disable()
|
||||
mock_call_pip.side_effect = PipFailed("Test error message")
|
||||
logging.disable() # A logging error message is expected here, but not desirable during test runs
|
||||
result = python_package_updates_are_available()
|
||||
self.assertEqual(result, False)
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
def test_parse_pip_list_output_no_input(self):
|
||||
results_dict = parse_pip_list_output("", "")
|
||||
self.assertEqual(len(results_dict), 0)
|
||||
|
||||
def test_parse_pip_list_output_all_packages_no_updates(self):
|
||||
results_dict = parse_pip_list_output(
|
||||
["Package Version", "---------- -------", "gitdb 4.0.9", "setuptools 41.2.0"],
|
||||
[],
|
||||
)
|
||||
self.assertEqual(len(results_dict), 2)
|
||||
self.assertTrue("gitdb" in results_dict)
|
||||
self.assertTrue("setuptools" in results_dict)
|
||||
self.assertEqual(results_dict["gitdb"]["installed_version"], "4.0.9")
|
||||
self.assertEqual(results_dict["gitdb"]["available_version"], "")
|
||||
self.assertEqual(results_dict["setuptools"]["installed_version"], "41.2.0")
|
||||
self.assertEqual(results_dict["setuptools"]["available_version"], "")
|
||||
|
||||
def test_parse_pip_list_output_all_packages_with_updates(self):
|
||||
results_dict = parse_pip_list_output(
|
||||
[],
|
||||
[
|
||||
"Package Version Latest Type",
|
||||
"---------- ------- ------ -----",
|
||||
"pip 21.0.1 22.1.2 wheel",
|
||||
"setuptools 41.2.0 63.2.0 wheel",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(results_dict), 2)
|
||||
self.assertTrue("pip" in results_dict)
|
||||
self.assertTrue("setuptools" in results_dict)
|
||||
self.assertEqual(results_dict["pip"]["installed_version"], "21.0.1")
|
||||
self.assertEqual(results_dict["pip"]["available_version"], "22.1.2")
|
||||
self.assertEqual(results_dict["setuptools"]["installed_version"], "41.2.0")
|
||||
self.assertEqual(results_dict["setuptools"]["available_version"], "63.2.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
QtCore.QTimer.singleShot(0, unittest.main)
|
||||
app.exec()
|
||||
@@ -1,131 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import functools
|
||||
import unittest
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from AddonManagerTest.gui.gui_mocks import (
|
||||
DialogWatcher,
|
||||
FakeWorker,
|
||||
MockThread,
|
||||
)
|
||||
from AddonManagerTest.app.mocks import MockAddon
|
||||
|
||||
from addonmanager_uninstaller_gui import AddonUninstallerGUI
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TestUninstallerGUI(unittest.TestCase):
|
||||
|
||||
MODULE = "test_uninstaller_gui" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.addon_to_remove = MockAddon()
|
||||
self.uninstaller_gui = AddonUninstallerGUI(self.addon_to_remove)
|
||||
self.finalized_thread = False
|
||||
self.signals_caught = []
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def catch_signal(self, signal_name, *_):
|
||||
self.signals_caught.append(signal_name)
|
||||
|
||||
def test_confirmation_dialog_yes(self):
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Confirm remove"),
|
||||
QtWidgets.QDialogButtonBox.Yes,
|
||||
)
|
||||
answer = self.uninstaller_gui._confirm_uninstallation()
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
self.assertTrue(answer, "Expected a 'Yes' click to return True, but got False")
|
||||
|
||||
def test_confirmation_dialog_cancel(self):
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Confirm remove"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
answer = self.uninstaller_gui._confirm_uninstallation()
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
self.assertFalse(answer, "Expected a 'Cancel' click to return False, but got True")
|
||||
|
||||
def test_progress_dialog(self):
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Removing Addon"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
self.uninstaller_gui._show_progress_dialog()
|
||||
# That call isn't modal, so spin our own event loop:
|
||||
while self.uninstaller_gui.progress_dialog.isVisible():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_timer_launches_progress_dialog(self):
|
||||
worker = FakeWorker()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Removing Addon"),
|
||||
QtWidgets.QDialogButtonBox.Cancel,
|
||||
)
|
||||
QtCore.QTimer.singleShot(1000, worker.stop) # If the test fails, this kills the "worker"
|
||||
self.uninstaller_gui._confirm_uninstallation = lambda: True
|
||||
self.uninstaller_gui._run_uninstaller = worker.work
|
||||
self.uninstaller_gui._finalize = lambda: None
|
||||
self.uninstaller_gui.dialog_timer.setInterval(1) # To speed up the test, only wait 1ms
|
||||
self.uninstaller_gui.run() # Blocks once it hits the fake worker
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
worker.stop()
|
||||
|
||||
def test_success_dialog(self):
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Uninstall complete"),
|
||||
QtWidgets.QDialogButtonBox.Ok,
|
||||
)
|
||||
self.uninstaller_gui._succeeded(self.addon_to_remove)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_failure_dialog(self):
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Uninstall failed"),
|
||||
QtWidgets.QDialogButtonBox.Ok,
|
||||
)
|
||||
self.uninstaller_gui._failed(
|
||||
self.addon_to_remove, "Some failure message\nAnother failure message"
|
||||
)
|
||||
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
|
||||
self.assertTrue(dialog_watcher.button_found, "Failed to find the expected button")
|
||||
|
||||
def test_finalize(self):
|
||||
self.uninstaller_gui.finished.connect(functools.partial(self.catch_signal, "finished"))
|
||||
self.uninstaller_gui.worker_thread = MockThread()
|
||||
self.uninstaller_gui._finalize()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
@@ -1,260 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2025 FreeCAD project association AISBL *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from time import sleep
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
from Addon import Addon
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
from addonmanager_update_all_gui import UpdateAllGUI, AddonStatus
|
||||
|
||||
|
||||
class MockUpdater(QtCore.QObject):
|
||||
success = QtCore.Signal(object)
|
||||
failure = QtCore.Signal(object)
|
||||
finished = QtCore.Signal()
|
||||
|
||||
def __init__(self, addon, addons=[]):
|
||||
super().__init__()
|
||||
self.addon_to_install = addon
|
||||
self.addons = addons
|
||||
self.has_run = False
|
||||
self.emit_success = True
|
||||
self.work_function = None # Set to some kind of callable to make this function take time
|
||||
|
||||
def run(self):
|
||||
self.has_run = True
|
||||
if self.work_function is not None and callable(self.work_function):
|
||||
self.work_function()
|
||||
if self.emit_success:
|
||||
self.success.emit(self.addon_to_install)
|
||||
else:
|
||||
self.failure.emit(self.addon_to_install)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class MockUpdaterFactory:
|
||||
def __init__(self, addons):
|
||||
self.addons = addons
|
||||
self.work_function = None
|
||||
self.updater = None
|
||||
|
||||
def get_updater(self, addon):
|
||||
self.updater = MockUpdater(addon, self.addons)
|
||||
self.updater.work_function = self.work_function
|
||||
return self.updater
|
||||
|
||||
|
||||
class MockAddon:
|
||||
def __init__(self, name):
|
||||
self.display_name = name
|
||||
self.name = name
|
||||
self.macro = None
|
||||
self.metadata = None
|
||||
self.installed_metadata = None
|
||||
|
||||
def status(self):
|
||||
return Addon.Status.UPDATE_AVAILABLE
|
||||
|
||||
|
||||
class CallInterceptor:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.args = None
|
||||
|
||||
def intercept(self, *args):
|
||||
self.called = True
|
||||
self.args = args
|
||||
|
||||
|
||||
class TestUpdateAllGui(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.addons = []
|
||||
for i in range(3):
|
||||
self.addons.append(MockAddon(f"Mock Addon {i}"))
|
||||
self.factory = MockUpdaterFactory(self.addons)
|
||||
self.test_object = UpdateAllGUI(self.addons)
|
||||
self.test_object.updater_factory = self.factory
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_run(self):
|
||||
self.factory.work_function = lambda: sleep(0.1)
|
||||
self.test_object.run()
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
|
||||
def test_setup_dialog(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.assertIsNotNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
)
|
||||
self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 3)
|
||||
|
||||
def test_cancelling_installation(self):
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
self.counter = 0
|
||||
self.LIMIT = 100
|
||||
self.limit_reached = False
|
||||
|
||||
def run(self):
|
||||
while self.counter < self.LIMIT:
|
||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||
return
|
||||
self.counter += 1
|
||||
sleep(0.01)
|
||||
self.limit_reached = True
|
||||
|
||||
worker = Worker()
|
||||
self.factory.work_function = worker.run
|
||||
self.test_object.run()
|
||||
cancel_timer = QtCore.QTimer()
|
||||
cancel_timer.timeout.connect(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).click
|
||||
)
|
||||
cancel_timer.start(90)
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 10)
|
||||
self.assertGreater(len(self.test_object.addons_with_update), 0)
|
||||
|
||||
def test_add_addon_to_table(self):
|
||||
mock_addon = MockAddon("MockAddon")
|
||||
self.test_object.dialog.tableWidget.clear()
|
||||
self.test_object._add_addon_to_table(mock_addon, 1)
|
||||
self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 1)
|
||||
|
||||
def test_update_addon_status(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_addon_status(0, AddonStatus.WAITING)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.WAITING.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.INSTALLING)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.SUCCEEDED)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.SUCCEEDED.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.FAILED)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.FAILED.ui_string(),
|
||||
)
|
||||
|
||||
def test_process_next_update(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._launch_active_installer = lambda: None
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(1, 2).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(2, 2).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
|
||||
def test_launch_active_installer(self):
|
||||
self.test_object.active_installer = self.factory.get_updater(self.addons[0])
|
||||
self.test_object._update_succeeded = lambda _: None
|
||||
self.test_object._update_failed = lambda _: None
|
||||
self.test_object.process_next_update = lambda: None
|
||||
self.test_object._launch_active_installer()
|
||||
# The above call does not block, so spin until it has completed (basically instantly in testing)
|
||||
while self.test_object.worker_thread.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
|
||||
def test_update_succeeded(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_succeeded(self.addons[0])
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.SUCCEEDED.ui_string(),
|
||||
)
|
||||
|
||||
def test_update_failed(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_failed(self.addons[0])
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 2).text(),
|
||||
AddonStatus.FAILED.ui_string(),
|
||||
)
|
||||
|
||||
def test_update_finished(self):
|
||||
self.test_object._setup_dialog()
|
||||
call_interceptor = CallInterceptor()
|
||||
self.test_object.worker_thread = QtCore.QThread()
|
||||
self.test_object.worker_thread.start()
|
||||
self.test_object._process_next_update = call_interceptor.intercept
|
||||
self.test_object.active_installer = self.factory.get_updater(self.addons[0])
|
||||
self.test_object._update_finished()
|
||||
self.assertFalse(self.test_object.worker_thread.isRunning())
|
||||
self.test_object.worker_thread.quit()
|
||||
self.assertTrue(call_interceptor.called)
|
||||
self.test_object.worker_thread.wait()
|
||||
|
||||
def test_finalize(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object.worker_thread = QtCore.QThread()
|
||||
self.test_object.worker_thread.start()
|
||||
self.test_object._finalize()
|
||||
self.assertFalse(self.test_object.worker_thread.isRunning())
|
||||
self.test_object.worker_thread.quit()
|
||||
self.test_object.worker_thread.wait()
|
||||
self.assertFalse(self.test_object.running)
|
||||
self.assertIsNotNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Close)
|
||||
)
|
||||
self.assertIsNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
)
|
||||
|
||||
def test_is_running(self):
|
||||
self.assertFalse(self.test_object.is_running())
|
||||
self.test_object.run()
|
||||
self.assertTrue(self.test_object.is_running())
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
@@ -1,145 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.append("../..")
|
||||
|
||||
from Widgets.addonmanager_widget_progress_bar import Progress
|
||||
|
||||
|
||||
class TestProgress(unittest.TestCase):
|
||||
|
||||
def test_default_construction(self):
|
||||
"""Given no parameters, a single-task Progress object is initialized with zero progress"""
|
||||
progress = Progress()
|
||||
self.assertEqual(progress.status_text, "")
|
||||
self.assertEqual(progress.number_of_tasks, 1)
|
||||
self.assertEqual(progress.current_task, 0)
|
||||
self.assertEqual(progress.current_task_progress, 0.0)
|
||||
|
||||
def test_good_parameters(self):
|
||||
"""Given good parameters, no exception is raised"""
|
||||
_ = Progress(
|
||||
status_text="Some text", number_of_tasks=1, current_task=0, current_task_progress=0.0
|
||||
)
|
||||
|
||||
def test_zero_task_count(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Progress(number_of_tasks=0)
|
||||
|
||||
def test_negative_task_count(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Progress(number_of_tasks=-1)
|
||||
|
||||
def test_setting_status_post_creation(self):
|
||||
progress = Progress()
|
||||
self.assertEqual(progress.status_text, "")
|
||||
progress.status_text = "Some status"
|
||||
self.assertEqual(progress.status_text, "Some status")
|
||||
|
||||
def test_setting_task_count(self):
|
||||
progress = Progress()
|
||||
progress.number_of_tasks = 10
|
||||
self.assertEqual(progress.number_of_tasks, 10)
|
||||
|
||||
def test_setting_negative_task_count(self):
|
||||
progress = Progress()
|
||||
with self.assertRaises(ValueError):
|
||||
progress.number_of_tasks = -1
|
||||
|
||||
def test_setting_invalid_task_count(self):
|
||||
progress = Progress()
|
||||
with self.assertRaises(TypeError):
|
||||
progress.number_of_tasks = 3.14159
|
||||
|
||||
def test_setting_current_task(self):
|
||||
progress = Progress(number_of_tasks=10)
|
||||
progress.number_of_tasks = 5
|
||||
self.assertEqual(progress.number_of_tasks, 5)
|
||||
|
||||
def test_setting_current_task_greater_than_task_count(self):
|
||||
progress = Progress()
|
||||
progress.number_of_tasks = 10
|
||||
with self.assertRaises(ValueError):
|
||||
progress.current_task = 11
|
||||
|
||||
def test_setting_current_task_equal_to_task_count(self):
|
||||
"""current_task is zero-indexed, so this is too high"""
|
||||
progress = Progress()
|
||||
progress.number_of_tasks = 10
|
||||
with self.assertRaises(ValueError):
|
||||
progress.current_task = 10
|
||||
|
||||
def test_setting_current_task_negative(self):
|
||||
progress = Progress()
|
||||
with self.assertRaises(ValueError):
|
||||
progress.current_task = -1
|
||||
|
||||
def test_setting_current_task_invalid(self):
|
||||
progress = Progress()
|
||||
with self.assertRaises(TypeError):
|
||||
progress.current_task = 2.718281
|
||||
|
||||
def test_setting_current_task_progress(self):
|
||||
progress = Progress()
|
||||
progress.current_task_progress = 50.0
|
||||
self.assertEqual(progress.current_task_progress, 50.0)
|
||||
|
||||
def test_setting_current_task_progress_too_low(self):
|
||||
progress = Progress()
|
||||
progress.current_task_progress = -0.01
|
||||
self.assertEqual(progress.current_task_progress, 0.0)
|
||||
|
||||
def test_setting_current_task_progress_too_high(self):
|
||||
progress = Progress()
|
||||
progress.current_task_progress = 100.001
|
||||
self.assertEqual(progress.current_task_progress, 100.0)
|
||||
|
||||
def test_incrementing_task(self):
|
||||
progress = Progress(number_of_tasks=10, current_task_progress=100.0)
|
||||
progress.next_task()
|
||||
self.assertEqual(progress.current_task, 1)
|
||||
self.assertEqual(progress.current_task_progress, 0.0)
|
||||
|
||||
def test_incrementing_task_too_high(self):
|
||||
progress = Progress(number_of_tasks=10, current_task=9, current_task_progress=100.0)
|
||||
with self.assertRaises(ValueError):
|
||||
progress.next_task()
|
||||
|
||||
def test_overall_progress_simple(self):
|
||||
progress = Progress()
|
||||
self.assertEqual(progress.overall_progress(), 0.0)
|
||||
|
||||
def test_overall_progress_with_ranges(self):
|
||||
progress = Progress(number_of_tasks=2, current_task=1, current_task_progress=0.0)
|
||||
self.assertAlmostEqual(progress.overall_progress(), 0.5)
|
||||
|
||||
def test_overall_progress_with_ranges_and_progress(self):
|
||||
progress = Progress(number_of_tasks=10, current_task=5, current_task_progress=50.0)
|
||||
self.assertAlmostEqual(progress.overall_progress(), 0.55)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,200 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import json
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
import NetworkManager
|
||||
from Addon import Addon
|
||||
from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
LoadPackagesFromCacheWorker,
|
||||
LoadMacrosFromCacheWorker,
|
||||
)
|
||||
|
||||
run_slow_tests = False
|
||||
|
||||
|
||||
class TestWorkersStartup(unittest.TestCase):
|
||||
|
||||
MODULE = "test_workers_startup" # file name without extension
|
||||
|
||||
@unittest.skipUnless(run_slow_tests, "This integration test is slow and uses the network")
|
||||
def setUp(self):
|
||||
"""Set up the test"""
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
self.saved_mod_directory = Addon.mod_directory
|
||||
self.saved_cache_directory = Addon.cache_directory
|
||||
Addon.mod_directory = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
Addon.cache_directory = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Cache")
|
||||
|
||||
os.makedirs(Addon.mod_directory, mode=0o777, exist_ok=True)
|
||||
os.makedirs(Addon.cache_directory, mode=0o777, exist_ok=True)
|
||||
|
||||
url = "https://api.github.com/zen"
|
||||
NetworkManager.InitializeNetworkManager()
|
||||
result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
|
||||
if result is None:
|
||||
self.skipTest("No active internet connection detected")
|
||||
|
||||
self.addon_list = []
|
||||
self.macro_counter = 0
|
||||
self.workbench_counter = 0
|
||||
self.prefpack_counter = 0
|
||||
self.addon_from_cache_counter = 0
|
||||
self.macro_from_cache_counter = 0
|
||||
|
||||
self.package_cache = {}
|
||||
self.macro_cache = []
|
||||
|
||||
self.package_cache_filename = os.path.join(Addon.cache_directory, "packages.json")
|
||||
self.macro_cache_filename = os.path.join(Addon.cache_directory, "macros.json")
|
||||
|
||||
# Store the user's preference for whether git is enabled or disabled
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
self.saved_git_disabled_status = pref.GetBool("disableGit", False)
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down the test"""
|
||||
Addon.mod_directory = self.saved_mod_directory
|
||||
Addon.cache_directory = self.saved_cache_directory
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetBool("disableGit", self.saved_git_disabled_status)
|
||||
|
||||
def test_create_addon_list_worker(self):
|
||||
"""Test whether any addons are added: runs the full query, so this potentially is a SLOW
|
||||
test."""
|
||||
worker = CreateAddonListWorker()
|
||||
worker.addon_repo.connect(self._addon_added)
|
||||
worker.start()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
self.assertGreater(self.macro_counter, 0, "No macros returned")
|
||||
self.assertGreater(self.workbench_counter, 0, "No workbenches returned")
|
||||
|
||||
# Make sure there are no duplicates:
|
||||
addon_name_set = set()
|
||||
for addon in self.addon_list:
|
||||
addon_name_set.add(addon.name)
|
||||
self.assertEqual(
|
||||
len(addon_name_set), len(self.addon_list), "Duplicate names are not allowed"
|
||||
)
|
||||
|
||||
# Write the cache data
|
||||
if hasattr(self, "package_cache"):
|
||||
with open(self.package_cache_filename, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(self.package_cache, indent=" "))
|
||||
if hasattr(self, "macro_cache"):
|
||||
with open(self.macro_cache_filename, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(self.macro_cache, indent=" "))
|
||||
|
||||
original_macro_counter = self.macro_counter
|
||||
original_addon_list = self.addon_list.copy()
|
||||
self.macro_counter = 0
|
||||
self.workbench_counter = 0
|
||||
self.addon_list.clear()
|
||||
|
||||
# Now try loading the same data from the cache we just created
|
||||
worker = LoadPackagesFromCacheWorker(self.package_cache_filename)
|
||||
worker.override_metadata_cache_path(os.path.join(Addon.cache_directory, "PackageMetadata"))
|
||||
worker.addon_repo.connect(self._addon_added)
|
||||
|
||||
worker.start()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
worker = LoadMacrosFromCacheWorker(self.macro_cache_filename)
|
||||
worker.add_macro_signal.connect(self._addon_added)
|
||||
|
||||
worker.start()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
# Make sure that every addon in the original list is also in the new list
|
||||
fail_counter = 0
|
||||
for original_addon in original_addon_list:
|
||||
found = False
|
||||
for addon in self.addon_list:
|
||||
if addon.name == original_addon.name:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
print(f"Failed to load {addon.name} from cache")
|
||||
fail_counter += 1
|
||||
self.assertEqual(fail_counter, 0)
|
||||
|
||||
# Make sure there are no duplicates:
|
||||
addon_name_set.clear()
|
||||
for addon in self.addon_list:
|
||||
addon_name_set.add(addon.name)
|
||||
|
||||
self.assertEqual(len(addon_name_set), len(self.addon_list))
|
||||
self.assertEqual(len(original_addon_list), len(self.addon_list))
|
||||
|
||||
self.assertEqual(
|
||||
original_macro_counter,
|
||||
self.macro_counter,
|
||||
"Cache loaded a different number of macros",
|
||||
)
|
||||
# We can't check workbench and preference pack counting at this point, because that relies
|
||||
# on the package.xml metadata file, which this test does not download.
|
||||
|
||||
def test_create_addon_list_git_disabled(self):
|
||||
"""If the user has git enabled, also test the addon manager with git disabled"""
|
||||
if self.saved_git_disabled_status:
|
||||
self.skipTest("Git is disabled, this test is redundant")
|
||||
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetBool("disableGit", True)
|
||||
|
||||
self.test_create_addon_list_worker()
|
||||
|
||||
def _addon_added(self, addon: Addon):
|
||||
"""Callback for adding an Addon: tracks the list, and counts the various types"""
|
||||
print(f"Addon added: {addon.name}")
|
||||
self.addon_list.append(addon)
|
||||
if addon.contains_workbench():
|
||||
self.workbench_counter += 1
|
||||
if addon.contains_macro():
|
||||
self.macro_counter += 1
|
||||
if addon.contains_preference_pack():
|
||||
self.prefpack_counter += 1
|
||||
|
||||
# Also record the information for cache purposes
|
||||
if addon.macro is None:
|
||||
self.package_cache[addon.name] = addon.to_cache()
|
||||
else:
|
||||
self.macro_cache.append(addon.macro.to_cache())
|
||||
@@ -1,78 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import FreeCAD
|
||||
from addonmanager_workers_utility import ConnectionChecker
|
||||
from PySide import QtCore
|
||||
|
||||
import NetworkManager
|
||||
|
||||
|
||||
class TestWorkersUtility(unittest.TestCase):
|
||||
|
||||
MODULE = "test_workers_utility" # file name without extension
|
||||
|
||||
@unittest.skip("Test is slow and uses the network: refactor!")
|
||||
def setUp(self):
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
self.last_result = None
|
||||
|
||||
url = "https://api.github.com/zen"
|
||||
NetworkManager.InitializeNetworkManager()
|
||||
result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
|
||||
if result is None:
|
||||
self.skipTest("No active internet connection detected")
|
||||
|
||||
def test_connection_checker_basic(self):
|
||||
"""Tests the connection checking worker's basic operation: does not exit until worker thread completes"""
|
||||
worker = ConnectionChecker()
|
||||
worker.success.connect(self.connection_succeeded)
|
||||
worker.failure.connect(self.connection_failed)
|
||||
self.last_result = None
|
||||
worker.start()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
self.assertEqual(self.last_result, "SUCCESS")
|
||||
|
||||
def test_connection_checker_thread_interrupt(self):
|
||||
worker = ConnectionChecker()
|
||||
worker.success.connect(self.connection_succeeded)
|
||||
worker.failure.connect(self.connection_failed)
|
||||
self.last_result = None
|
||||
worker.start()
|
||||
worker.requestInterruption()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
self.assertIsNone(self.last_result, "Requesting interruption of thread failed to interrupt")
|
||||
|
||||
def connection_succeeded(self):
|
||||
self.last_result = "SUCCESS"
|
||||
|
||||
def connection_failed(self):
|
||||
self.last_result = "FAILURE"
|
||||
@@ -1,3 +0,0 @@
|
||||
## Unit tests for the Addon Manager
|
||||
|
||||
Data files are located in the `data/` subdirectory.
|
||||
@@ -1,81 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2024 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Classes and structures related to Addon sidecar information"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import addonmanager_freecad_interface as fci
|
||||
|
||||
|
||||
def to_int_or_zero(inp: [str | int | None]):
|
||||
try:
|
||||
return int(inp)
|
||||
except TypeError:
|
||||
return 0
|
||||
|
||||
|
||||
def time_string_to_datetime(inp: str) -> Optional[datetime]:
|
||||
try:
|
||||
return datetime.fromisoformat(inp)
|
||||
except ValueError:
|
||||
try:
|
||||
# Support for the trailing "Z" was added in Python 3.11 -- strip it and see if it works now
|
||||
return datetime.fromisoformat(inp[:-1])
|
||||
except ValueError:
|
||||
fci.Console.PrintWarning(f"Unable to parse '{str}' as a Python datetime")
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddonStats:
|
||||
"""Statistics about an addon: not all stats apply to all addon types"""
|
||||
|
||||
last_update_time: datetime | None = None
|
||||
date_created: datetime | None = None
|
||||
stars: int = 0
|
||||
open_issues: int = 0
|
||||
forks: int = 0
|
||||
license: str = ""
|
||||
page_views_last_month: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_dict: dict):
|
||||
new_stats = AddonStats()
|
||||
if "pushed_at" in json_dict:
|
||||
new_stats.last_update_time = time_string_to_datetime(json_dict["pushed_at"])
|
||||
if "created_at" in json_dict:
|
||||
new_stats.date_created = time_string_to_datetime(json_dict["created_at"])
|
||||
if "stargazers_count" in json_dict:
|
||||
new_stats.stars = to_int_or_zero(json_dict["stargazers_count"])
|
||||
if "forks_count" in json_dict:
|
||||
new_stats.forks = to_int_or_zero(json_dict["forks_count"])
|
||||
if "open_issues_count" in json_dict:
|
||||
new_stats.open_issues = to_int_or_zero(json_dict["open_issues_count"])
|
||||
if "license" in json_dict:
|
||||
if json_dict["license"] != "NOASSERTION" and json_dict["license"] != "None":
|
||||
new_stats.license = json_dict["license"] # Might be None or "NOASSERTION"
|
||||
return new_stats
|
||||
@@ -1,189 +0,0 @@
|
||||
IF (BUILD_GUI)
|
||||
PYSIDE_WRAP_RC(AddonManager_QRC_SRCS Resources/AddonManager.qrc)
|
||||
add_subdirectory(Widgets)
|
||||
ENDIF (BUILD_GUI)
|
||||
|
||||
SET(AddonManager_SRCS
|
||||
ALLOWED_PYTHON_PACKAGES.txt
|
||||
Addon.py
|
||||
AddonManager.py
|
||||
AddonManager.ui
|
||||
AddonManagerOptions.py
|
||||
AddonManagerOptions.ui
|
||||
AddonManagerOptions_AddCustomRepository.ui
|
||||
AddonStats.py
|
||||
Init.py
|
||||
InitGui.py
|
||||
NetworkManager.py
|
||||
PythonDependencyUpdateDialog.ui
|
||||
TestAddonManagerApp.py
|
||||
add_toolbar_button_dialog.ui
|
||||
addonmanager_cache.py
|
||||
addonmanager_connection_checker.py
|
||||
addonmanager_dependency_installer.py
|
||||
addonmanager_devmode.py
|
||||
addonmanager_devmode_add_content.py
|
||||
addonmanager_devmode_license_selector.py
|
||||
addonmanager_devmode_licenses_table.py
|
||||
addonmanager_devmode_metadata_checker.py
|
||||
addonmanager_devmode_people_table.py
|
||||
addonmanager_devmode_person_editor.py
|
||||
addonmanager_devmode_predictor.py
|
||||
addonmanager_devmode_validators.py
|
||||
addonmanager_firstrun.py
|
||||
addonmanager_freecad_interface.py
|
||||
addonmanager_git.py
|
||||
addonmanager_installer.py
|
||||
addonmanager_installer_gui.py
|
||||
addonmanager_licenses.py
|
||||
addonmanager_macro.py
|
||||
addonmanager_macro_parser.py
|
||||
addonmanager_metadata.py
|
||||
addonmanager_package_details_controller.py
|
||||
addonmanager_preferences_defaults.json
|
||||
addonmanager_pyside_interface.py
|
||||
addonmanager_python_deps_gui.py
|
||||
addonmanager_readme_controller.py
|
||||
addonmanager_uninstaller.py
|
||||
addonmanager_uninstaller_gui.py
|
||||
addonmanager_update_all_gui.py
|
||||
addonmanager_utilities.py
|
||||
addonmanager_workers_installation.py
|
||||
addonmanager_workers_startup.py
|
||||
addonmanager_workers_utility.py
|
||||
change_branch.py
|
||||
change_branch.ui
|
||||
compact_view.py
|
||||
composite_view.py
|
||||
dependency_resolution_dialog.ui
|
||||
developer_mode.ui
|
||||
developer_mode_add_content.ui
|
||||
developer_mode_advanced_freecad_versions.ui
|
||||
developer_mode_copyright_info.ui
|
||||
developer_mode_dependencies.ui
|
||||
developer_mode_edit_dependency.ui
|
||||
developer_mode_freecad_versions.ui
|
||||
developer_mode_license.ui
|
||||
developer_mode_licenses_table.ui
|
||||
developer_mode_people.ui
|
||||
developer_mode_people_table.ui
|
||||
developer_mode_select_from_list.ui
|
||||
developer_mode_tags.ui
|
||||
expanded_view.py
|
||||
first_run.ui
|
||||
install_to_toolbar.py
|
||||
loading.html
|
||||
package_list.py
|
||||
select_toolbar_dialog.ui
|
||||
update_all.ui
|
||||
)
|
||||
IF (BUILD_GUI)
|
||||
LIST(APPEND AddonManager_SRCS TestAddonManagerGui.py)
|
||||
ENDIF (BUILD_GUI)
|
||||
|
||||
SOURCE_GROUP("" FILES ${AddonManager_SRCS})
|
||||
|
||||
SET(AddonManagerTests_SRCS
|
||||
AddonManagerTest/__init__.py
|
||||
AddonManagerTest/test_information.md
|
||||
)
|
||||
|
||||
SET(AddonManagerTestsApp_SRCS
|
||||
AddonManagerTest/app/__init__.py
|
||||
AddonManagerTest/app/mocks.py
|
||||
AddonManagerTest/app/test_addon.py
|
||||
AddonManagerTest/app/test_addoncatalog.py
|
||||
AddonManagerTest/app/test_cache.py
|
||||
AddonManagerTest/app/test_dependency_installer.py
|
||||
AddonManagerTest/app/test_freecad_interface.py
|
||||
AddonManagerTest/app/test_git.py
|
||||
AddonManagerTest/app/test_installer.py
|
||||
AddonManagerTest/app/test_macro.py
|
||||
AddonManagerTest/app/test_macro_parser.py
|
||||
AddonManagerTest/app/test_metadata.py
|
||||
AddonManagerTest/app/test_uninstaller.py
|
||||
AddonManagerTest/app/test_utilities.py
|
||||
)
|
||||
|
||||
SET(AddonManagerTestsGui_SRCS
|
||||
AddonManagerTest/gui/__init__.py
|
||||
AddonManagerTest/gui/gui_mocks.py
|
||||
AddonManagerTest/gui/test_gui.py
|
||||
AddonManagerTest/gui/test_installer_gui.py
|
||||
AddonManagerTest/gui/test_python_deps_gui.py
|
||||
AddonManagerTest/gui/test_uninstaller_gui.py
|
||||
AddonManagerTest/gui/test_update_all_gui.py
|
||||
AddonManagerTest/gui/test_widget_progress_bar.py
|
||||
AddonManagerTest/gui/test_workers_startup.py
|
||||
AddonManagerTest/gui/test_workers_utility.py
|
||||
)
|
||||
|
||||
SET(AddonManagerTestsFiles_SRCS
|
||||
AddonManagerTest/data/__init__.py
|
||||
AddonManagerTest/data/addon_update_stats.json
|
||||
AddonManagerTest/data/bundle_only.xml
|
||||
AddonManagerTest/data/combination.xml
|
||||
AddonManagerTest/data/corrupted_metadata.zip
|
||||
AddonManagerTest/data/depends_on_all_workbenches.xml
|
||||
AddonManagerTest/data/DoNothing.FCMacro
|
||||
AddonManagerTest/data/git_submodules.txt
|
||||
AddonManagerTest/data/good_package.xml
|
||||
AddonManagerTest/data/icon_cache.zip
|
||||
AddonManagerTest/data/icon_cache.zip.sha1
|
||||
AddonManagerTest/data/macro_only.xml
|
||||
AddonManagerTest/data/macro_template.FCStd
|
||||
AddonManagerTest/data/MacrosRecipesWikiPage.zip
|
||||
AddonManagerTest/data/metadata.zip
|
||||
AddonManagerTest/data/missing_macro_metadata.FCStd
|
||||
AddonManagerTest/data/other_only.xml
|
||||
AddonManagerTest/data/prefpack_only.xml
|
||||
AddonManagerTest/data/test_addon_with_fcmacro.zip
|
||||
AddonManagerTest/data/test_github_style_repo.zip
|
||||
AddonManagerTest/data/test_repo.zip
|
||||
AddonManagerTest/data/test_simple_repo.zip
|
||||
AddonManagerTest/data/test_version_detection.xml
|
||||
AddonManagerTest/data/TestWorkbench.zip
|
||||
AddonManagerTest/data/workbench_only.xml
|
||||
)
|
||||
|
||||
SET(AddonManagerTests_ALL
|
||||
${AddonManagerTests_SRCS}
|
||||
${AddonManagerTestsApp_SRCS}
|
||||
${AddonManagerTestsFiles_SRCS}
|
||||
)
|
||||
|
||||
IF (BUILD_GUI)
|
||||
LIST(APPEND AddonManagerTests_ALL ${AddonManagerTestsGui_SRCS})
|
||||
ENDIF (BUILD_GUI)
|
||||
|
||||
ADD_CUSTOM_TARGET(AddonManager ALL
|
||||
SOURCES ${AddonManager_SRCS} ${AddonManager_QRC_SRCS}
|
||||
)
|
||||
|
||||
ADD_CUSTOM_TARGET(AddonManagerTests ALL
|
||||
SOURCES ${AddonManagerTests_ALL}
|
||||
)
|
||||
|
||||
fc_copy_sources(AddonManager "${CMAKE_BINARY_DIR}/Mod/AddonManager" ${AddonManager_SRCS})
|
||||
|
||||
fc_copy_sources(AddonManagerTests "${CMAKE_BINARY_DIR}/Mod/AddonManager" ${AddonManagerTests_ALL})
|
||||
|
||||
IF (BUILD_GUI)
|
||||
fc_target_copy_resource(AddonManager
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_BINARY_DIR}/Mod/AddonManager
|
||||
AddonManager_rc.py)
|
||||
ENDIF (BUILD_GUI)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${AddonManager_SRCS}
|
||||
${AddonManager_QRC_SRCS}
|
||||
DESTINATION
|
||||
Mod/AddonManager
|
||||
)
|
||||
|
||||
INSTALL(FILES ${AddonManagerTests_SRCS} DESTINATION Mod/AddonManager/AddonManagerTest)
|
||||
INSTALL(FILES ${AddonManagerTestsApp_SRCS} DESTINATION Mod/AddonManager/AddonManagerTest/app)
|
||||
INSTALL(FILES ${AddonManagerTestsGui_SRCS} DESTINATION Mod/AddonManager/AddonManagerTest/gui)
|
||||
INSTALL(FILES ${AddonManagerTestsFiles_SRCS} DESTINATION Mod/AddonManager/AddonManagerTest/data)
|
||||
@@ -1,8 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# FreeCAD init script of the AddonManager module
|
||||
# (c) 2001 Juergen Riegel
|
||||
# License LGPL
|
||||
|
||||
import FreeCAD
|
||||
|
||||
FreeCAD.__unit_test__ += ["TestAddonManagerApp"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# AddonManager gui init module
|
||||
# (c) 2001 Juergen Riegel
|
||||
# License LGPL
|
||||
|
||||
import AddonManager
|
||||
|
||||
FreeCADGui.addLanguagePath(":/translations")
|
||||
FreeCADGui.addCommand("Std_AddonMgr", AddonManager.CommandAddonManager())
|
||||
|
||||
import FreeCAD
|
||||
|
||||
FreeCAD.__unit_test__ += ["TestAddonManagerGui"]
|
||||
@@ -1,714 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""
|
||||
#############################################################################
|
||||
#
|
||||
# ABOUT NETWORK MANAGER
|
||||
#
|
||||
# A wrapper around QNetworkAccessManager providing proxy-handling
|
||||
# capabilities, and simplified access to submitting requests from any
|
||||
# application thread.
|
||||
#
|
||||
#
|
||||
# USAGE
|
||||
#
|
||||
# Once imported, this file provides access to a global object called
|
||||
# AM_NETWORK_MANAGER. This is a QObject running on the main thread, but
|
||||
# designed to be interacted with from any other application thread. It
|
||||
# provides two principal methods: submit_unmonitored_get() and
|
||||
# submit_monitored_get(). Use the unmonitored version for small amounts of
|
||||
# data (suitable for caching in RAM, and without a need to show a progress
|
||||
# bar during download), and the monitored version for larger amounts of data.
|
||||
# Both functions take a URL, and return an integer index. That index allows
|
||||
# tracking of the completed request by attaching to the signals completed(),
|
||||
# progress_made(), and progress_complete(). All three provide, as the first
|
||||
# argument to the signal, the index of the request the signal refers to.
|
||||
# Code attached to those signals should filter them to look for the indices
|
||||
# of the requests they care about. Requests may complete in any order.
|
||||
#
|
||||
# A secondary blocking interface is also provided, for very short network
|
||||
# accesses: the blocking_get() function blocks until the network transmission
|
||||
# is complete, directly returning a QByteArray object with the received data.
|
||||
# Do not run on the main GUI thread!
|
||||
"""
|
||||
|
||||
import threading
|
||||
import os
|
||||
import queue
|
||||
import itertools
|
||||
import tempfile
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
import FreeCAD
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
|
||||
HAVE_FREECAD = True
|
||||
translate = FreeCAD.Qt.translate
|
||||
except ImportError:
|
||||
# For standalone testing support working without the FreeCAD import
|
||||
HAVE_FREECAD = False
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
from PySide import QtWidgets
|
||||
|
||||
|
||||
# This is the global instance of the NetworkManager that outside code
|
||||
# should access
|
||||
AM_NETWORK_MANAGER = None
|
||||
|
||||
HAVE_QTNETWORK = True
|
||||
try:
|
||||
from PySide import QtNetwork
|
||||
except ImportError:
|
||||
if HAVE_FREECAD:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
'Could not import QtNetwork -- it does not appear to be installed on your system. Your provider may have a package for this dependency (often called "python3-pyside2.qtnetwork")',
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
else:
|
||||
print("Could not import QtNetwork, unable to test this file.")
|
||||
sys.exit(1)
|
||||
HAVE_QTNETWORK = False
|
||||
|
||||
if HAVE_QTNETWORK:
|
||||
|
||||
# Added in Qt 5.15
|
||||
if hasattr(QtNetwork.QNetworkRequest, "DefaultTransferTimeoutConstant"):
|
||||
timeoutConstant = QtNetwork.QNetworkRequest.DefaultTransferTimeoutConstant
|
||||
if hasattr(timeoutConstant, "value"):
|
||||
# Qt 6 changed the timeout constant to have a 'value' attribute.
|
||||
# The function setTransferTimeout does not accept
|
||||
# DefaultTransferTimeoutConstant of type
|
||||
# QtNetwork.QNetworkRequest.TransferTimeoutConstant any
|
||||
# longer but only an int.
|
||||
default_timeout = timeoutConstant.value
|
||||
else:
|
||||
# In Qt 5.15 we can use the timeoutConstant as is.
|
||||
default_timeout = timeoutConstant
|
||||
else:
|
||||
default_timeout = 30000
|
||||
|
||||
class QueueItem:
|
||||
"""A container for information about an item in the network queue."""
|
||||
|
||||
def __init__(self, index: int, request: QtNetwork.QNetworkRequest, track_progress: bool):
|
||||
self.index = index
|
||||
self.request = request
|
||||
self.original_url = request.url()
|
||||
self.track_progress = track_progress
|
||||
|
||||
class NetworkManager(QtCore.QObject):
|
||||
"""A single global instance of NetworkManager is instantiated and stored as
|
||||
AM_NETWORK_MANAGER. Outside threads should send GET requests to this class by
|
||||
calling the submit_unmonitored_request() or submit_monitored_request() function,
|
||||
as needed. See the documentation of those functions for details."""
|
||||
|
||||
# Connect to complete for requests with no progress monitoring (e.g. small amounts of data)
|
||||
completed = QtCore.Signal(
|
||||
int, int, QtCore.QByteArray
|
||||
) # Index, http response code, received data (if any)
|
||||
|
||||
# Connect to progress_made and progress_complete for large amounts of data, which get buffered into a temp file
|
||||
# That temp file should be deleted when your code is done with it
|
||||
progress_made = QtCore.Signal(int, int, int) # Index, bytes read, total bytes (may be None)
|
||||
|
||||
progress_complete = QtCore.Signal(
|
||||
int, int, os.PathLike
|
||||
) # Index, http response code, filename
|
||||
|
||||
__request_queued = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.counting_iterator = itertools.count()
|
||||
self.queue = queue.Queue()
|
||||
self.__last_started_index = 0
|
||||
self.__abort_when_found: List[int] = []
|
||||
self.replies: Dict[int, QtNetwork.QNetworkReply] = {}
|
||||
self.file_buffers = {}
|
||||
|
||||
# We support an arbitrary number of threads using synchronous GET calls:
|
||||
self.synchronous_lock = threading.Lock()
|
||||
self.synchronous_complete: Dict[int, bool] = {}
|
||||
self.synchronous_result_data: Dict[int, QtCore.QByteArray] = {}
|
||||
|
||||
# Make sure we exit nicely on quit
|
||||
if QtCore.QCoreApplication.instance() is not None:
|
||||
QtCore.QCoreApplication.instance().aboutToQuit.connect(self.__aboutToQuit)
|
||||
|
||||
# Create the QNAM on this thread:
|
||||
self.QNAM = QtNetwork.QNetworkAccessManager()
|
||||
self.QNAM.proxyAuthenticationRequired.connect(self.__authenticate_proxy)
|
||||
self.QNAM.authenticationRequired.connect(self.__authenticate_resource)
|
||||
self.QNAM.setRedirectPolicy(QtNetwork.QNetworkRequest.ManualRedirectPolicy)
|
||||
|
||||
qnam_cache = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.CacheLocation)
|
||||
os.makedirs(qnam_cache, exist_ok=True)
|
||||
self.diskCache = QtNetwork.QNetworkDiskCache()
|
||||
self.diskCache.setCacheDirectory(qnam_cache)
|
||||
self.QNAM.setCache(self.diskCache)
|
||||
|
||||
self.monitored_connections: List[int] = []
|
||||
self._setup_proxy()
|
||||
|
||||
# A helper connection for our blocking interface
|
||||
self.completed.connect(self.__synchronous_process_completion)
|
||||
|
||||
# Set up our worker connection
|
||||
self.__request_queued.connect(self.__setup_network_request)
|
||||
|
||||
def _setup_proxy(self):
|
||||
"""Set up the proxy based on user preferences or prompts on command line"""
|
||||
|
||||
# Set up the proxy, if necessary:
|
||||
if HAVE_FREECAD:
|
||||
(
|
||||
noProxyCheck,
|
||||
systemProxyCheck,
|
||||
userProxyCheck,
|
||||
proxy_string,
|
||||
) = self._setup_proxy_freecad()
|
||||
else:
|
||||
(
|
||||
noProxyCheck,
|
||||
systemProxyCheck,
|
||||
userProxyCheck,
|
||||
proxy_string,
|
||||
) = self._setup_proxy_standalone()
|
||||
|
||||
if noProxyCheck:
|
||||
pass
|
||||
elif systemProxyCheck:
|
||||
query = QtNetwork.QNetworkProxyQuery(
|
||||
QtCore.QUrl("https://github.com/FreeCAD/FreeCAD")
|
||||
)
|
||||
proxy = QtNetwork.QNetworkProxyFactory.systemProxyForQuery(query)
|
||||
if proxy and proxy[0]:
|
||||
self.QNAM.setProxy(proxy[0]) # This may still be QNetworkProxy.NoProxy
|
||||
elif userProxyCheck:
|
||||
try:
|
||||
parsed_url = urlparse(proxy_string)
|
||||
host = parsed_url.hostname
|
||||
port = parsed_url.port
|
||||
scheme = (
|
||||
"http" if parsed_url.scheme == "https" else parsed_url.scheme
|
||||
) # There seems no https type: doc.qt.io/qt-6/qnetworkproxy.html#ProxyType-enum
|
||||
except ValueError:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Failed to parse proxy URL '{}'",
|
||||
).format(proxy_string)
|
||||
+ "\n"
|
||||
)
|
||||
return
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Using proxy {scheme}://{host}:{port} \n")
|
||||
if scheme == "http":
|
||||
_scheme = QtNetwork.QNetworkProxy.HttpProxy
|
||||
elif scheme == "socks5":
|
||||
_scheme = QtNetwork.QNetworkProxy.Socks5Proxy
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(f"Unknown proxy scheme '{scheme}', using http. \n")
|
||||
_scheme = QtNetwork.QNetworkProxy.HttpProxy
|
||||
proxy = QtNetwork.QNetworkProxy(_scheme, host, port)
|
||||
self.QNAM.setProxy(proxy)
|
||||
|
||||
def _setup_proxy_freecad(self):
|
||||
"""If we are running within FreeCAD, this uses the config data to set up the proxy"""
|
||||
noProxyCheck = True
|
||||
systemProxyCheck = False
|
||||
userProxyCheck = False
|
||||
proxy_string = ""
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
noProxyCheck = pref.GetBool("NoProxyCheck", noProxyCheck)
|
||||
systemProxyCheck = pref.GetBool("SystemProxyCheck", systemProxyCheck)
|
||||
userProxyCheck = pref.GetBool("UserProxyCheck", userProxyCheck)
|
||||
proxy_string = pref.GetString("ProxyUrl", "")
|
||||
|
||||
# Add some error checking to the proxy setup, since for historical reasons they
|
||||
# are independent booleans, rather than an enumeration:
|
||||
option_count = [noProxyCheck, systemProxyCheck, userProxyCheck].count(True)
|
||||
if option_count != 1:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Parameter error: mutually exclusive proxy options set. Resetting to default.",
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
noProxyCheck = True
|
||||
systemProxyCheck = False
|
||||
userProxyCheck = False
|
||||
pref.SetBool("NoProxyCheck", noProxyCheck)
|
||||
pref.SetBool("SystemProxyCheck", systemProxyCheck)
|
||||
pref.SetBool("UserProxyCheck", userProxyCheck)
|
||||
|
||||
if userProxyCheck and not proxy_string:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Parameter error: user proxy indicated, but no proxy provided. Resetting to default.",
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
noProxyCheck = True
|
||||
userProxyCheck = False
|
||||
pref.SetBool("NoProxyCheck", noProxyCheck)
|
||||
pref.SetBool("UserProxyCheck", userProxyCheck)
|
||||
return noProxyCheck, systemProxyCheck, userProxyCheck, proxy_string
|
||||
|
||||
def _setup_proxy_standalone(self):
|
||||
"""If we are NOT running inside FreeCAD, prompt the user for proxy information"""
|
||||
noProxyCheck = True
|
||||
systemProxyCheck = False
|
||||
userProxyCheck = False
|
||||
proxy_string = ""
|
||||
print("Please select a proxy type:")
|
||||
print("1) No proxy")
|
||||
print("2) Use system proxy settings")
|
||||
print("3) Custom proxy settings")
|
||||
result = input("Choice: ")
|
||||
if result == "1":
|
||||
pass
|
||||
elif result == "2":
|
||||
noProxyCheck = False
|
||||
systemProxyCheck = True
|
||||
elif result == "3":
|
||||
noProxyCheck = False
|
||||
userProxyCheck = True
|
||||
proxy_string = input("Enter your proxy server (host:port): ")
|
||||
else:
|
||||
print(f"Got {result}, expected 1, 2, or 3.")
|
||||
app.quit()
|
||||
return noProxyCheck, systemProxyCheck, userProxyCheck, proxy_string
|
||||
|
||||
def __aboutToQuit(self):
|
||||
"""Called when the application is about to quit. Not currently used."""
|
||||
|
||||
def __setup_network_request(self):
|
||||
"""Get the next request off the queue and launch it."""
|
||||
try:
|
||||
item = self.queue.get_nowait()
|
||||
if item:
|
||||
if item.index in self.__abort_when_found:
|
||||
self.__abort_when_found.remove(item.index)
|
||||
return # Do not do anything with this item, it's been aborted...
|
||||
if item.track_progress:
|
||||
self.monitored_connections.append(item.index)
|
||||
self.__launch_request(item.index, item.request)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def __launch_request(self, index: int, request: QtNetwork.QNetworkRequest) -> None:
|
||||
"""Given a network request, ask the QNetworkAccessManager to begin processing it."""
|
||||
reply = self.QNAM.get(request)
|
||||
self.replies[index] = reply
|
||||
|
||||
self.__last_started_index = index
|
||||
reply.finished.connect(self.__reply_finished)
|
||||
reply.sslErrors.connect(self.__on_ssl_error)
|
||||
if index in self.monitored_connections:
|
||||
reply.readyRead.connect(self.__ready_to_read)
|
||||
reply.downloadProgress.connect(self.__download_progress)
|
||||
|
||||
def submit_unmonitored_get(
|
||||
self,
|
||||
url: str,
|
||||
timeout_ms: int = default_timeout,
|
||||
) -> int:
|
||||
"""Adds this request to the queue, and returns an index that can be used by calling code
|
||||
in conjunction with the completed() signal to handle the results of the call. All data is
|
||||
kept in memory, and the completed() call includes a direct handle to the bytes returned. It
|
||||
is not called until the data transfer has finished and the connection is closed."""
|
||||
|
||||
current_index = next(self.counting_iterator) # A thread-safe counter
|
||||
# Use a queue because we can only put things on the QNAM from the main event loop thread
|
||||
self.queue.put(
|
||||
QueueItem(
|
||||
current_index, self.__create_get_request(url, timeout_ms), track_progress=False
|
||||
)
|
||||
)
|
||||
self.__request_queued.emit()
|
||||
return current_index
|
||||
|
||||
def submit_monitored_get(
|
||||
self,
|
||||
url: str,
|
||||
timeout_ms: int = default_timeout,
|
||||
) -> int:
|
||||
"""Adds this request to the queue, and returns an index that can be used by calling code
|
||||
in conjunction with the progress_made() and progress_completed() signals to handle the
|
||||
results of the call. All data is cached to disk, and progress is reported periodically
|
||||
as the underlying QNetworkReply reports its progress. The progress_completed() signal
|
||||
contains a path to a temporary file with the stored data. Calling code should delete this
|
||||
file when done with it (or move it into its final place, etc.)."""
|
||||
|
||||
current_index = next(self.counting_iterator) # A thread-safe counter
|
||||
# Use a queue because we can only put things on the QNAM from the main event loop thread
|
||||
self.queue.put(
|
||||
QueueItem(
|
||||
current_index, self.__create_get_request(url, timeout_ms), track_progress=True
|
||||
)
|
||||
)
|
||||
self.__request_queued.emit()
|
||||
return current_index
|
||||
|
||||
def blocking_get(
|
||||
self,
|
||||
url: str,
|
||||
timeout_ms: int = default_timeout,
|
||||
) -> Optional[QtCore.QByteArray]:
|
||||
"""Submits a GET request to the QNetworkAccessManager and block until it is complete"""
|
||||
|
||||
current_index = next(self.counting_iterator) # A thread-safe counter
|
||||
with self.synchronous_lock:
|
||||
self.synchronous_complete[current_index] = False
|
||||
|
||||
self.queue.put(
|
||||
QueueItem(
|
||||
current_index, self.__create_get_request(url, timeout_ms), track_progress=False
|
||||
)
|
||||
)
|
||||
self.__request_queued.emit()
|
||||
while True:
|
||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||
return None
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
with self.synchronous_lock:
|
||||
if self.synchronous_complete[current_index]:
|
||||
break
|
||||
|
||||
with self.synchronous_lock:
|
||||
self.synchronous_complete.pop(current_index)
|
||||
if current_index in self.synchronous_result_data:
|
||||
return self.synchronous_result_data.pop(current_index)
|
||||
return None
|
||||
|
||||
def __synchronous_process_completion(
|
||||
self, index: int, code: int, data: QtCore.QByteArray
|
||||
) -> None:
|
||||
"""Check the return status of a completed process, and handle its returned data (if
|
||||
any)."""
|
||||
with self.synchronous_lock:
|
||||
if index in self.synchronous_complete:
|
||||
if code == 200:
|
||||
self.synchronous_result_data[index] = data
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Addon Manager: Unexpected {} response from server",
|
||||
).format(code)
|
||||
+ "\n"
|
||||
)
|
||||
self.synchronous_complete[index] = True
|
||||
|
||||
@staticmethod
|
||||
def __create_get_request(url: str, timeout_ms: int) -> QtNetwork.QNetworkRequest:
|
||||
"""Construct a network request to a given URL"""
|
||||
request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
|
||||
request.setAttribute(
|
||||
QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
|
||||
QtNetwork.QNetworkRequest.ManualRedirectPolicy,
|
||||
)
|
||||
request.setAttribute(QtNetwork.QNetworkRequest.CacheSaveControlAttribute, True)
|
||||
request.setAttribute(
|
||||
QtNetwork.QNetworkRequest.CacheLoadControlAttribute,
|
||||
QtNetwork.QNetworkRequest.PreferNetwork,
|
||||
)
|
||||
if hasattr(request, "setTransferTimeout"):
|
||||
# Added in Qt 5.15
|
||||
# In Qt 5, the function setTransferTimeout seems to accept
|
||||
# DefaultTransferTimeoutConstant of type
|
||||
# PySide2.QtNetwork.QNetworkRequest.TransferTimeoutConstant,
|
||||
# whereas in Qt 6, the function seems to only accept an
|
||||
# integer.
|
||||
request.setTransferTimeout(timeout_ms)
|
||||
return request
|
||||
|
||||
def abort_all(self):
|
||||
"""Abort ALL network calls in progress, including clearing the queue"""
|
||||
for reply in self.replies.values():
|
||||
if reply.abort().isRunning():
|
||||
reply.abort()
|
||||
while True:
|
||||
try:
|
||||
self.queue.get()
|
||||
self.queue.task_done()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
def abort(self, index: int):
|
||||
"""Abort a specific request"""
|
||||
if index in self.replies and self.replies[index].isRunning():
|
||||
self.replies[index].abort()
|
||||
elif index < self.__last_started_index:
|
||||
# It's still in the queue. Mark it for later destruction.
|
||||
self.__abort_when_found.append(index)
|
||||
|
||||
def __authenticate_proxy(
|
||||
self,
|
||||
reply: QtNetwork.QNetworkProxy,
|
||||
authenticator: QtNetwork.QAuthenticator,
|
||||
):
|
||||
"""If proxy authentication is required, attempt to authenticate. If the GUI is running this displays
|
||||
a window asking for credentials. If the GUI is not running, it prompts on the command line.
|
||||
"""
|
||||
if HAVE_FREECAD and FreeCAD.GuiUp:
|
||||
proxy_authentication = FreeCADGui.PySideUic.loadUi(
|
||||
os.path.join(os.path.dirname(__file__), "proxy_authentication.ui")
|
||||
)
|
||||
# Show the right labels, etc.
|
||||
proxy_authentication.labelProxyAddress.setText(f"{reply.hostName()}:{reply.port()}")
|
||||
if authenticator.realm():
|
||||
proxy_authentication.labelProxyRealm.setText(authenticator.realm())
|
||||
else:
|
||||
proxy_authentication.labelProxyRealm.hide()
|
||||
proxy_authentication.labelRealmCaption.hide()
|
||||
result = proxy_authentication.exec()
|
||||
if result == QtWidgets.QDialogButtonBox.Ok:
|
||||
authenticator.setUser(proxy_authentication.lineEditUsername.text())
|
||||
authenticator.setPassword(proxy_authentication.lineEditPassword.text())
|
||||
else:
|
||||
username = input("Proxy username: ")
|
||||
import getpass
|
||||
|
||||
password = getpass.getpass()
|
||||
authenticator.setUser(username)
|
||||
authenticator.setPassword(password)
|
||||
|
||||
def __authenticate_resource(
|
||||
self,
|
||||
_reply: QtNetwork.QNetworkReply,
|
||||
_authenticator: QtNetwork.QAuthenticator,
|
||||
):
|
||||
"""Unused."""
|
||||
|
||||
def __on_ssl_error(self, reply: str, errors: List[str] = None):
|
||||
"""Called when an SSL error occurs: prints the error information."""
|
||||
if HAVE_FREECAD:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate("AddonsInstaller", "Error with encrypted connection") + "\n:"
|
||||
)
|
||||
FreeCAD.Console.PrintWarning(reply)
|
||||
if errors is not None:
|
||||
for error in errors:
|
||||
FreeCAD.Console.PrintWarning(error)
|
||||
else:
|
||||
print("Error with encrypted connection")
|
||||
if errors is not None:
|
||||
for error in errors:
|
||||
print(error)
|
||||
|
||||
def __download_progress(self, bytesReceived: int, bytesTotal: int) -> None:
|
||||
"""Monitors download progress and emits a progress_made signal"""
|
||||
sender = self.sender()
|
||||
if not sender:
|
||||
return
|
||||
for index, reply in self.replies.items():
|
||||
if reply == sender:
|
||||
self.progress_made.emit(index, bytesReceived, bytesTotal)
|
||||
return
|
||||
|
||||
def __ready_to_read(self) -> None:
|
||||
"""Called when data is available, this reads that data."""
|
||||
sender = self.sender()
|
||||
if not sender:
|
||||
return
|
||||
|
||||
for index, reply in self.replies.items():
|
||||
if reply == sender:
|
||||
self.__data_incoming(index, reply)
|
||||
return
|
||||
|
||||
def __data_incoming(self, index: int, reply: QtNetwork.QNetworkReply) -> None:
|
||||
"""Read incoming data and attach it to a data object"""
|
||||
if not index in self.replies:
|
||||
# We already finished this reply, this is a vestigial signal
|
||||
return
|
||||
buffer = reply.readAll()
|
||||
if not index in self.file_buffers:
|
||||
f = tempfile.NamedTemporaryFile("wb", delete=False)
|
||||
self.file_buffers[index] = f
|
||||
else:
|
||||
f = self.file_buffers[index]
|
||||
try:
|
||||
f.write(buffer.data())
|
||||
except OSError as e:
|
||||
if HAVE_FREECAD:
|
||||
FreeCAD.Console.PrintError(f"Network Manager internal error: {str(e)}")
|
||||
else:
|
||||
print(f"Network Manager internal error: {str(e)}")
|
||||
|
||||
def __reply_finished(self) -> None:
|
||||
"""Called when a reply has been completed: this makes sure the data has been read and
|
||||
any notifications have been called."""
|
||||
reply = self.sender()
|
||||
if not reply:
|
||||
# This can happen during a cancellation operation: silently do nothing
|
||||
return
|
||||
|
||||
index = None
|
||||
for key, value in self.replies.items():
|
||||
if reply == value:
|
||||
index = key
|
||||
break
|
||||
if index is None:
|
||||
return
|
||||
|
||||
response_code = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
redirect_codes = [301, 302, 303, 305, 307, 308]
|
||||
if response_code in redirect_codes: # This is a redirect
|
||||
timeout_ms = default_timeout
|
||||
if hasattr(reply, "request"):
|
||||
request = reply.request()
|
||||
if hasattr(request, "transferTimeout"):
|
||||
timeout_ms = request.transferTimeout()
|
||||
new_url = reply.attribute(QtNetwork.QNetworkRequest.RedirectionTargetAttribute)
|
||||
self.__launch_request(index, self.__create_get_request(new_url, timeout_ms))
|
||||
return # The task is not done, so get out of this method now
|
||||
if reply.error() != QtNetwork.QNetworkReply.NetworkError.OperationCanceledError:
|
||||
# It this was not a timeout, make sure we mark the queue task done
|
||||
self.queue.task_done()
|
||||
if reply.error() == QtNetwork.QNetworkReply.NetworkError.NoError:
|
||||
if index in self.monitored_connections:
|
||||
# Make sure to read any remaining data
|
||||
self.__data_incoming(index, reply)
|
||||
self.monitored_connections.remove(index)
|
||||
f = self.file_buffers[index]
|
||||
f.close()
|
||||
self.progress_complete.emit(index, response_code, f.name)
|
||||
else:
|
||||
data = reply.readAll()
|
||||
self.completed.emit(index, response_code, data)
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(f"Request failed: {reply.error()} \n")
|
||||
if index in self.monitored_connections:
|
||||
self.progress_complete.emit(index, response_code, "")
|
||||
else:
|
||||
self.completed.emit(index, response_code, None)
|
||||
self.replies.pop(index)
|
||||
|
||||
else: # HAVE_QTNETWORK is false:
|
||||
|
||||
class NetworkManager(QtCore.QObject):
|
||||
"""A dummy class to enable an offline mode when the QtNetwork package is not yet installed"""
|
||||
|
||||
completed = QtCore.Signal(
|
||||
int, int, bytes
|
||||
) # Emitted as soon as the request is made, with a connection failed error
|
||||
progress_made = QtCore.Signal(int, int, int) # Never emitted, no progress is made here
|
||||
progress_complete = QtCore.Signal(
|
||||
int, int, os.PathLike
|
||||
) # Emitted as soon as the request is made, with a connection failed error
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.monitored_queue = queue.Queue()
|
||||
self.unmonitored_queue = queue.Queue()
|
||||
|
||||
def submit_unmonitored_request(self, _) -> int:
|
||||
"""Returns a fake index that can be used for testing -- nothing is actually queued"""
|
||||
current_index = next(itertools.count())
|
||||
self.unmonitored_queue.put(current_index)
|
||||
return current_index
|
||||
|
||||
def submit_monitored_request(self, _) -> int:
|
||||
"""Returns a fake index that can be used for testing -- nothing is actually queued"""
|
||||
current_index = next(itertools.count())
|
||||
self.monitored_queue.put(current_index)
|
||||
return current_index
|
||||
|
||||
def blocking_get(self, _: str) -> QtCore.QByteArray:
|
||||
"""No operation - returns None immediately"""
|
||||
return None
|
||||
|
||||
def abort_all(
|
||||
self,
|
||||
):
|
||||
"""There is nothing to abort in this case"""
|
||||
|
||||
def abort(self, _):
|
||||
"""There is nothing to abort in this case"""
|
||||
|
||||
|
||||
def InitializeNetworkManager():
|
||||
"""Called once at the beginning of program execution to create the appropriate manager object"""
|
||||
global AM_NETWORK_MANAGER
|
||||
if AM_NETWORK_MANAGER is None:
|
||||
AM_NETWORK_MANAGER = NetworkManager()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QtCore.QCoreApplication()
|
||||
|
||||
InitializeNetworkManager()
|
||||
|
||||
count = 0
|
||||
|
||||
# For testing, create several network requests and send them off in quick succession:
|
||||
# (Choose small downloads, no need for significant data)
|
||||
urls = [
|
||||
"https://api.github.com/zen",
|
||||
"http://climate.ok.gov/index.php/climate/rainfall_table/local_data",
|
||||
"https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/AIANNHA/MapServer",
|
||||
]
|
||||
|
||||
def handle_completion(index: int, code: int, data):
|
||||
"""Attached to the completion signal, prints diagnostic information about the network access"""
|
||||
global count
|
||||
if code == 200:
|
||||
print(f"For request {index+1}, response was {data.size()} bytes.", flush=True)
|
||||
else:
|
||||
print(
|
||||
f"For request {index+1}, request failed with HTTP result code {code}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
count += 1
|
||||
if count >= len(urls):
|
||||
print("Shutting down...", flush=True)
|
||||
AM_NETWORK_MANAGER.requestInterruption()
|
||||
AM_NETWORK_MANAGER.wait(5000)
|
||||
app.quit()
|
||||
|
||||
AM_NETWORK_MANAGER.completed.connect(handle_completion)
|
||||
for test_url in urls:
|
||||
AM_NETWORK_MANAGER.submit_unmonitored_get(test_url)
|
||||
|
||||
app.exec_()
|
||||
|
||||
print("Done with all requests.")
|
||||
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PythonDependencyUpdateDialog</class>
|
||||
<widget class="QDialog" name="PythonDependencyUpdateDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>528</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Manage Python Dependencies</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>The following Python packages have been installed locally by the Addon Manager to satisfy Addon dependencies. Installation location:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelInstallationPath">
|
||||
<property name="text">
|
||||
<string notr="true">placeholder for path</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="tableWidget">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Package name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Installed version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Available version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Used by</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>An asterisk (*) in the "Used by" column indicates an optional dependency. Note that Used by only records direct imports in the Addon. Other Python packages that those packages depend upon may have been installed as well.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUpdateAll">
|
||||
<property name="text">
|
||||
<string>Update all available</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,127 +0,0 @@
|
||||
<RCC>
|
||||
<qresource>
|
||||
<file>icons/preferences-addon_manager.svg</file>
|
||||
<file>icons/3D_Printing_Tools_workbench_icon.svg</file>
|
||||
<file>icons/AddonMgrWithWarning.svg</file>
|
||||
<file>icons/A2plus_workbench_icon.svg</file>
|
||||
<file>icons/AirPlaneDesign_workbench_icon.svg</file>
|
||||
<file>icons/ArchTextures_workbench_icon.svg</file>
|
||||
<file>icons/Assembly4_workbench_icon.svg</file>
|
||||
<file>icons/BCFPlugin_workbench_icon.svg</file>
|
||||
<file>icons/BIMBots_workbench_icon.svg</file>
|
||||
<file>icons/BIM_workbench_icon.svg</file>
|
||||
<file>icons/BOLTSFC_workbench_icon.svg</file>
|
||||
<file>icons/CADExchanger_workbench_icon.svg</file>
|
||||
<file>icons/cadquery_module_workbench_icon.svg</file>
|
||||
<file>icons/Cfd_workbench_icon.svg</file>
|
||||
<file>icons/CfdOF_workbench_icon.svg</file>
|
||||
<file>icons/CurvedShapes_workbench_icon.svg</file>
|
||||
<file>icons/Curves_workbench_icon.svg</file>
|
||||
<file>icons/Defeaturing_workbench_icon.svg</file>
|
||||
<file>icons/DesignSPHysics_workbench_icon.svg</file>
|
||||
<file>icons/dodo_workbench_icon.svg</file>
|
||||
<file>icons/DynamicData_workbench_icon.svg</file>
|
||||
<file>icons/EM_workbench_icon.svg</file>
|
||||
<file>icons/ExplodedAssembly_workbench_icon.svg</file>
|
||||
<file>icons/ExtMan_workbench_icon.svg</file>
|
||||
<file>icons/fasteners_workbench_icon.svg</file>
|
||||
<file>icons/flamingo_workbench_icon.svg</file>
|
||||
<file>icons/FEM_FrontISTR_workbench_icon.svg</file>
|
||||
<file>icons/GDML_workbench_icon.svg</file>
|
||||
<file>icons/GDT_workbench_icon.svg</file>
|
||||
<file>icons/FCGear_workbench_icon.svg</file>
|
||||
<file>icons/Geomatics_workbench_icon.svg</file>
|
||||
<file>icons/ImportNURBS_workbench_icon.svg</file>
|
||||
<file>icons/InventorLoader_workbench_icon.svg</file>
|
||||
<file>icons/kicadStepUpMod_workbench_icon.svg</file>
|
||||
<file>icons/lattice2_workbench_icon.svg</file>
|
||||
<file>icons/LCInterlocking_workbench_icon.svg</file>
|
||||
<file>icons/Lithophane_workbench_icon.svg</file>
|
||||
<file>icons/Maker_workbench_icon.svg</file>
|
||||
<file>icons/Manipulator_workbench_icon.svg</file>
|
||||
<file>icons/Marz_workbench_icon.svg</file>
|
||||
<file>icons/MeshRemodel_workbench_icon.svg</file>
|
||||
<file>icons/ModernUI_workbench_icon.svg</file>
|
||||
<file>icons/MOOC_workbench_icon.svg</file>
|
||||
<file>icons/MnesarcoUtils_workbench_icon.svg</file>
|
||||
<file>icons/OSE3dPrinter_workbench_icon.svg</file>
|
||||
<file>icons/Part-o-magic_workbench_icon.svg</file>
|
||||
<file>icons/Plot_workbench_icon.svg</file>
|
||||
<file>icons/POV-Ray-Rendering_workbench_icon.svg</file>
|
||||
<file>icons/Pyramids-and-Polyhedrons_workbench_icon.svg</file>
|
||||
<file>icons/pyrate_workbench_icon.svg</file>
|
||||
<file>icons/Reinforcement_workbench_icon.svg</file>
|
||||
<file>icons/Reporting_workbench_icon.svg</file>
|
||||
<file>icons/Render_workbench_icon.svg</file>
|
||||
<file>icons/Rocket_workbench_icon.svg</file>
|
||||
<file>icons/sheetmetal_workbench_icon.svg</file>
|
||||
<file>icons/slic3r-tools_workbench_icon.svg</file>
|
||||
<file>icons/Ship_workbench_icon.svg</file>
|
||||
<file>icons/Silk_workbench_icon.svg</file>
|
||||
<file>icons/TaackPLM_workbench_icon.svg</file>
|
||||
<file>icons/timber_workbench_icon.svg</file>
|
||||
<file>icons/ThreadProfile_workbench_icon.svg</file>
|
||||
<file>icons/WebTools_workbench_icon.svg</file>
|
||||
<file>icons/workfeature_workbench_icon.svg</file>
|
||||
<file>icons/yaml-workspace_workbench_icon.svg</file>
|
||||
<file>icons/compact_view.svg</file>
|
||||
<file>icons/composite_view.svg</file>
|
||||
<file>icons/expanded_view.svg</file>
|
||||
<file>icons/sort_ascending.svg</file>
|
||||
<file>icons/sort_descending.svg</file>
|
||||
<file>licenses/Apache-2.0.txt</file>
|
||||
<file>licenses/BSD-2-Clause.txt</file>
|
||||
<file>licenses/BSD-3-Clause.txt</file>
|
||||
<file>licenses/CC0-1.0.txt</file>
|
||||
<file>licenses/GPL-2.0-or-later.txt</file>
|
||||
<file>licenses/GPL-3.0-or-later.txt</file>
|
||||
<file>licenses/LGPL-2.1-or-later.txt</file>
|
||||
<file>licenses/LGPL-3.0-or-later.txt</file>
|
||||
<file>licenses/MIT.txt</file>
|
||||
<file>licenses/MPL-2.0.txt</file>
|
||||
<file>licenses/spdx.json</file>
|
||||
<file>translations/AddonManager_af.qm</file>
|
||||
<file>translations/AddonManager_ar.qm</file>
|
||||
<file>translations/AddonManager_ca.qm</file>
|
||||
<file>translations/AddonManager_cs.qm</file>
|
||||
<file>translations/AddonManager_de.qm</file>
|
||||
<file>translations/AddonManager_el.qm</file>
|
||||
<file>translations/AddonManager_es-ES.qm</file>
|
||||
<file>translations/AddonManager_eu.qm</file>
|
||||
<file>translations/AddonManager_fi.qm</file>
|
||||
<file>translations/AddonManager_fil.qm</file>
|
||||
<file>translations/AddonManager_fr.qm</file>
|
||||
<file>translations/AddonManager_gl.qm</file>
|
||||
<file>translations/AddonManager_hr.qm</file>
|
||||
<file>translations/AddonManager_hu.qm</file>
|
||||
<file>translations/AddonManager_id.qm</file>
|
||||
<file>translations/AddonManager_it.qm</file>
|
||||
<file>translations/AddonManager_ja.qm</file>
|
||||
<file>translations/AddonManager_kab.qm</file>
|
||||
<file>translations/AddonManager_ko.qm</file>
|
||||
<file>translations/AddonManager_lt.qm</file>
|
||||
<file>translations/AddonManager_nl.qm</file>
|
||||
<file>translations/AddonManager_no.qm</file>
|
||||
<file>translations/AddonManager_pl.qm</file>
|
||||
<file>translations/AddonManager_pt-BR.qm</file>
|
||||
<file>translations/AddonManager_pt-PT.qm</file>
|
||||
<file>translations/AddonManager_ro.qm</file>
|
||||
<file>translations/AddonManager_ru.qm</file>
|
||||
<file>translations/AddonManager_sk.qm</file>
|
||||
<file>translations/AddonManager_sl.qm</file>
|
||||
<file>translations/AddonManager_sr.qm</file>
|
||||
<file>translations/AddonManager_sv-SE.qm</file>
|
||||
<file>translations/AddonManager_tr.qm</file>
|
||||
<file>translations/AddonManager_uk.qm</file>
|
||||
<file>translations/AddonManager_val-ES.qm</file>
|
||||
<file>translations/AddonManager_vi.qm</file>
|
||||
<file>translations/AddonManager_zh-CN.qm</file>
|
||||
<file>translations/AddonManager_zh-TW.qm</file>
|
||||
<file>translations/AddonManager_es-AR.qm</file>
|
||||
<file>translations/AddonManager_bg.qm</file>
|
||||
<file>translations/AddonManager_ka.qm</file>
|
||||
<file>translations/AddonManager_sr-CS.qm</file>
|
||||
<file>translations/AddonManager_be.qm</file>
|
||||
<file>translations/AddonManager_da.qm</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -1,179 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="Scale_Mesh.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="36.112677 : -87.557014 : 1"
|
||||
inkscape:vp_y="-4360.1598 : 2974.7292 : 0"
|
||||
inkscape:vp_z="268.67971 : 140.87747 : 1"
|
||||
inkscape:persp3d-origin="198.90457 : -5.0702273 : 1"
|
||||
id="perspective851" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6"
|
||||
inkscape:cx="-50.392857"
|
||||
inkscape:cy="31.05133"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
units="px"
|
||||
inkscape:snap-nodes="false" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-280.06665)">
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot3725"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion3727"><rect
|
||||
id="rect3729"
|
||||
width="62.857143"
|
||||
height="405.71429"
|
||||
x="240"
|
||||
y="362.51968" /></flowRegion><flowPara
|
||||
id="flowPara3731" /></flowRoot> <flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot4572"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion4574"><rect
|
||||
id="rect4576"
|
||||
width="271.42856"
|
||||
height="147.14285"
|
||||
x="67.14286"
|
||||
y="216.8054" /></flowRegion><flowPara
|
||||
id="flowPara4578" /></flowRoot> <rect
|
||||
style="fill:none;fill-opacity:1;stroke:none;stroke-width:148.89118958;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect834"
|
||||
width="64"
|
||||
height="64"
|
||||
x="1.6348703e-006"
|
||||
y="280.06665" />
|
||||
<g
|
||||
sodipodi:type="inkscape:box3d"
|
||||
id="g853"
|
||||
style="fill:#e5ff80;fill-opacity:1;stroke:none;stroke-width:37.79527664;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:perspectiveID="#perspective851"
|
||||
inkscape:corner0="1.2974359 : 0.051199999 : 0 : 1"
|
||||
inkscape:corner7="0.70666667 : 0.040472381 : 0.29286865 : 1">
|
||||
<path
|
||||
sodipodi:type="inkscape:box3dside"
|
||||
id="path855"
|
||||
style="fill:#353564;fill-rule:evenodd;stroke:none;stroke-width:194.0941925;stroke-linejoin:round"
|
||||
inkscape:box3dsidetype="6"
|
||||
d="M 9.8014818,329.42585 30.160757,343.31602 57.128521,327.4728 39.071136,315.15311 Z"
|
||||
points="30.160757,343.31602 57.128521,327.4728 39.071136,315.15311 9.8014818,329.42585 " />
|
||||
<path
|
||||
sodipodi:type="inkscape:box3dside"
|
||||
id="path865"
|
||||
style="fill:#e9e9ff;fill-rule:evenodd;stroke:none;stroke-width:194.0941925;stroke-linejoin:round"
|
||||
inkscape:box3dsidetype="11"
|
||||
d="m 39.071136,315.15311 0.874086,-34.41161 23.392499,15.95959 -6.2092,30.77171 z"
|
||||
points="39.945222,280.7415 63.337721,296.70109 57.128521,327.4728 39.071136,315.15311 " />
|
||||
<path
|
||||
sodipodi:type="inkscape:box3dside"
|
||||
id="path857"
|
||||
style="fill:#4d4d9f;fill-rule:evenodd;stroke:none;stroke-width:194.0941925;stroke-linejoin:round"
|
||||
inkscape:box3dsidetype="5"
|
||||
d="M 9.8014818,329.42585 0.69376043,294.04969 39.945222,280.7415 39.071136,315.15311 Z"
|
||||
points="0.69376043,294.04969 39.945222,280.7415 39.071136,315.15311 9.8014818,329.42585 " />
|
||||
<path
|
||||
sodipodi:type="inkscape:box3dside"
|
||||
id="path863"
|
||||
style="fill:#8ec98d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:194.0941925;stroke-linejoin:round"
|
||||
inkscape:box3dsidetype="13"
|
||||
d="m 30.160757,343.31602 -2.060281,-30.56804 35.237245,-16.04689 -6.2092,30.77171 z"
|
||||
points="28.100476,312.74798 63.337721,296.70109 57.128521,327.4728 30.160757,343.31602 " />
|
||||
<path
|
||||
sodipodi:type="inkscape:box3dside"
|
||||
id="path861"
|
||||
style="fill:#ee9bfc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:194.0941925;stroke-linejoin:round"
|
||||
inkscape:box3dsidetype="14"
|
||||
d="M 0.69376043,294.04969 28.100476,312.74798 63.337721,296.70109 39.945222,280.7415 Z"
|
||||
points="28.100476,312.74798 63.337721,296.70109 39.945222,280.7415 0.69376043,294.04969 " />
|
||||
<path
|
||||
sodipodi:type="inkscape:box3dside"
|
||||
id="path859"
|
||||
style="fill:#a7d9f0;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:194.0941925;stroke-linejoin:round"
|
||||
inkscape:box3dsidetype="3"
|
||||
d="M 9.8014818,329.42585 0.69376043,294.04969 28.100476,312.74798 30.160757,343.31602 Z"
|
||||
points="0.69376043,294.04969 28.100476,312.74798 30.160757,343.31602 9.8014818,329.42585 " />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.45151734;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1.7257872,295.06561 28.19751,313.19314 30.187504,342.8281 10.522794,329.36192 Z"
|
||||
id="path833"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 0.69376043,294.04969 39.945222,280.7415 63.337721,296.70109 28.100476,312.74798 v 0 0"
|
||||
id="path835"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 57.128521,327.4728 6.2092,-30.77171"
|
||||
id="path837"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 30.160757,343.31602 57.128521,327.4728"
|
||||
id="path839"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 0.69376043,294.04969 62.64396057,2.6514"
|
||||
id="path841"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 30.160757,343.31602 33.176964,-46.61493 v 0 0"
|
||||
id="path843"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.47181559;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 30.709234,343.85948 1.9309977,295.29193"
|
||||
id="path845"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,300 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 63.999999 64"
|
||||
sodipodi:docname="airplaneWBiconV2.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
inkscape:export-filename="C:\Users\Rafael\bitmap.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300">
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs6">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient913">
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop909" />
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop911" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4739">
|
||||
<stop
|
||||
style="stop-color:#204a87;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4735" />
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4737" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3797">
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3799-3" />
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3801" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3797"
|
||||
id="linearGradient4687-1-6-6"
|
||||
x1="18.145235"
|
||||
y1="51.499367"
|
||||
x2="44.398445"
|
||||
y2="51.499367"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1,0,0,1.0981138,0.14321474,-55.839871)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3797"
|
||||
id="linearGradient4695-7-8-7-0"
|
||||
x1="0.18017258"
|
||||
y1="36.248028"
|
||||
x2="14.623517"
|
||||
y2="36.248028"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-1,0,0,1,57.200391,-43.587124)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3797"
|
||||
id="linearGradient4687-1-6-6-8"
|
||||
x1="18.145235"
|
||||
y1="51.499367"
|
||||
x2="44.398445"
|
||||
y2="51.499367"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1,0,0,1.0981138,45.686839,-56.465414)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3797"
|
||||
id="linearGradient1065"
|
||||
x1="26.11158"
|
||||
y1="2.9369314"
|
||||
x2="44.096264"
|
||||
y2="2.9110429"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.2002734,0,0,1.2002734,-6.0365755,41.424104)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3797"
|
||||
id="linearGradient4655"
|
||||
x1="108"
|
||||
y1="46"
|
||||
x2="128"
|
||||
y2="46"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-86.020077,-13.687753)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4739"
|
||||
id="linearGradient995"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="39.971127"
|
||||
y1="44.082279"
|
||||
x2="26.592251"
|
||||
y2="23.73741"
|
||||
gradientTransform="translate(78.922405,4.6396909)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4739"
|
||||
id="linearGradient949"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="18.702328"
|
||||
y1="31.725029"
|
||||
x2="45.765244"
|
||||
y2="31.812008" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient913"
|
||||
id="linearGradient915"
|
||||
x1="43.590008"
|
||||
y1="61.390144"
|
||||
x2="22.400524"
|
||||
y2="2.3465822"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1800"
|
||||
inkscape:window-height="1096"
|
||||
id="namedview4"
|
||||
showgrid="true"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="10.374285"
|
||||
inkscape:cy="-2.7321467"
|
||||
inkscape:current-layer="g4675"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="214"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-to-guides="true"
|
||||
inkscape:snap-grids="false">
|
||||
<sodipodi:guide
|
||||
position="31.999999,59.249999"
|
||||
orientation="1,0"
|
||||
id="guide4620"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="2.03125,61.3125"
|
||||
orientation="1,0"
|
||||
id="guide853"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="62.004426,63.94897"
|
||||
orientation="1,0"
|
||||
id="guide855"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="8.53125,2"
|
||||
orientation="0,1"
|
||||
id="guide857"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="1.09375,62.000001"
|
||||
orientation="0,1"
|
||||
id="guide859"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="44,31"
|
||||
orientation="0,1"
|
||||
id="guide861"
|
||||
inkscape:locked="false" />
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid863"
|
||||
empspacing="2" />
|
||||
<sodipodi:guide
|
||||
position="41.125,18.125"
|
||||
orientation="0,1"
|
||||
id="guide916"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="81.624999,50.124999"
|
||||
orientation="0,1"
|
||||
id="guide918"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="4.6249999,67.125"
|
||||
orientation="1,0"
|
||||
id="guide1533"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="59.875,71.124999"
|
||||
orientation="1,0"
|
||||
id="guide1535"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="28.5,50.875"
|
||||
orientation="1,0"
|
||||
id="guide1537"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="36.749999,67.625"
|
||||
orientation="1,0"
|
||||
id="guide1539"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="-8.6249999,39.25"
|
||||
orientation="0,1"
|
||||
id="guide1541"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<g
|
||||
id="g4675"
|
||||
transform="matrix(1.0161989,0,0,1.0161989,-0.46489119,-0.38459578)">
|
||||
<g
|
||||
id="g1045"
|
||||
transform="translate(27.100757,23.156998)" />
|
||||
<path
|
||||
style="fill:#34e0e2;stroke:#042a2a;stroke-width:1.96811864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 60.471247,4.6806294 3.6892581,27.998455 18.531887,36.116006 Z"
|
||||
id="path5174"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#06989a;stroke:#042a2a;stroke-width:1.96811864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 18.531887,36.116006 27.080367,59.750028 28.503171,40.673222 60.471247,4.6806294 Z"
|
||||
id="path5178"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#042a2a;stroke:#042a2a;stroke-width:1.96811864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 27.080367,59.750028 60.471247,4.6806294 28.503171,40.673222 Z"
|
||||
id="path5180"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#16d0d2;stroke:#042a2a;stroke-width:1.96811864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 60.471247,4.6806294 28.503171,40.673222 58.606838,49.028083 Z"
|
||||
id="path5176"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#16d0d2;stroke:#34e0e2;stroke-width:2.00869703;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 57.899299,11.044025 -25.195091,28.366947 23.725688,6.58474 z"
|
||||
id="path5176-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#34e0e2;stroke-width:1.96811867;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 53.810206,12.110056 -32.891771,24.698235 6.4519,17.963692"
|
||||
id="path980"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#042a2a;stroke-width:1.96811867;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 27.100887,59.176296 28.606846,40.423251"
|
||||
id="path982"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#042a2a;stroke-width:1.96811867;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 44.903942,22.206144 57.418099,8.1263571"
|
||||
id="path986"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.8 KiB |
@@ -1,216 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.1 r15371"
|
||||
sodipodi:docname="Workbench.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="-157.07953"
|
||||
inkscape:cy="154.35889"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4485"
|
||||
empspacing="2"
|
||||
empcolor="#3f3fff"
|
||||
empopacity="0.28627451"
|
||||
color="#3f3fff"
|
||||
opacity="0.09411765" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-280.06665)">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#171018;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1.8520833,296.20623 H 14.552083 l 0,-10.05416 H 1.8520836 Z"
|
||||
id="path4491"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#171018;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1.8520836,286.15207 4.2333331,-4.7625 h 4.7625003 l 3.704166,4.7625 z"
|
||||
id="path4493"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ad7fa8;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2.38125,295.67707 h 11.641666 v -8.99584 H 2.38125 Z"
|
||||
id="path4495"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#171018;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1"
|
||||
d="m 3.4395833,296.20623 h 2.1166666 v -3.70416 H 3.4395833 Z"
|
||||
id="path4499"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#75507b;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 3.7041666,286.94582 h 1.0583333 v 1.05833 H 3.7041666 Z"
|
||||
id="path4501"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4503"
|
||||
d="m 2.6458334,288.00415 h 1.0583333 v 1.05833 H 2.6458334 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 3.7041667,289.06249 H 4.7625 v 1.05833 H 3.7041667 Z"
|
||||
id="path4505"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4507"
|
||||
d="m 4.7625,288.00415 h 1.0583333 v 1.05833 H 4.7625 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 5.8208334,286.94582 h 1.0583333 v 1.05833 H 5.8208334 Z"
|
||||
id="path4509"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4511"
|
||||
d="m 2.6458334,290.12082 h 1.0583333 v 1.05833 H 2.6458334 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4513"
|
||||
d="m 7.9374999,286.94582 h 1.0583333 v 1.05833 H 7.9374999 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 6.8791667,288.00415 H 7.9375 v 1.05833 H 6.8791667 Z"
|
||||
id="path4515"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4517"
|
||||
d="M 3.7041667,291.17916 H 4.7625 v 1.05833 H 3.7041667 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 4.7625,290.12082 h 1.0583337 v 1.05833 H 4.7625 Z"
|
||||
id="path4519"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4521"
|
||||
d="m 5.8208337,289.06249 h 1.058333 v 1.05833 h -1.058333 z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4525"
|
||||
d="m 5.2916665,283.24165 h 1.0583333 v 1.05833 H 5.2916665 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 4.2333333,284.29998 h 1.0583333 v 1.05833 H 4.2333333 Z"
|
||||
id="path4527"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 6.35,284.29998 h 1.0583333 v 1.05833 H 6.35 Z"
|
||||
id="path4529"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4531"
|
||||
d="m 6.3500001,282.18332 h 1.0583333 v 1.05833 H 6.3500001 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4533"
|
||||
d="m 7.4083334,283.24165 h 1.0583333 v 1.05833 H 7.4083334 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 8.4666667,282.18332 H 9.525 v 1.05833 H 8.4666667 Z"
|
||||
id="path4535"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ad7fa8;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 2.9104166,285.6229 H 13.49375 l -2.910417,-3.70417 H 6.35 Z"
|
||||
id="path4497"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 10.054167,286.94582 H 11.1125 v 1.05833 h -1.058333 z"
|
||||
id="path4539"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4541"
|
||||
d="m 8.9958333,288.00415 h 1.0583337 v 1.05833 H 8.9958333 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 5.8208333,291.17916 h 1.0583333 v 1.05833 H 5.8208333 Z"
|
||||
id="path4543"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4545"
|
||||
d="m 6.8791666,290.12082 h 1.0583337 v 1.05833 H 6.8791666 Z"
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#75507b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 7.9375003,289.06249 h 1.058333 v 1.05833 h -1.058333 z"
|
||||
id="path4547"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#171018;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 7.6729167,292.50207 h 2.1166664 v 1.5875 H 7.6729167 Z"
|
||||
id="path4549"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4551"
|
||||
d="m 10.847917,292.50207 h 2.116666 v 1.5875 h -2.116666 z"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#171018;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,411 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
id="svg3559"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="Model.svg"
|
||||
viewBox="0 0 64 64">
|
||||
<defs
|
||||
id="defs3561">
|
||||
<linearGradient
|
||||
id="linearGradient4383-1"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
id="stop69725"
|
||||
offset="0"
|
||||
style="stop-color:#c4a000;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop69727"
|
||||
offset="1"
|
||||
style="stop-color:#fce94f;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4383">
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4385" />
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4387" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4383-1"
|
||||
id="linearGradient4389"
|
||||
x1="20.243532"
|
||||
y1="37.588112"
|
||||
x2="17.243532"
|
||||
y2="27.588112"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-1.243533,-2.588112)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient73208"
|
||||
id="linearGradient4399"
|
||||
x1="48.714352"
|
||||
y1="45.585785"
|
||||
x2="44.714352"
|
||||
y2="34.585785"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(1.2856487,1.4142136)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4383-3"
|
||||
id="linearGradient4389-0"
|
||||
x1="27.243532"
|
||||
y1="54.588112"
|
||||
x2="21.243532"
|
||||
y2="30.588112"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-1.243533,-2.588112)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4383-3">
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4385-1" />
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4387-2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4393-9"
|
||||
id="linearGradient4399-7"
|
||||
x1="48.714352"
|
||||
y1="45.585785"
|
||||
x2="40.714352"
|
||||
y2="24.585787"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(1.2856487,1.4142136)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4393-9">
|
||||
<stop
|
||||
style="stop-color:#204a87;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4395-8" />
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4397-1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient73208"
|
||||
id="linearGradient69042"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-12.714351,-17.585786)"
|
||||
x1="48.714352"
|
||||
y1="45.585785"
|
||||
x2="44.714352"
|
||||
y2="34.585785" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4383-1"
|
||||
id="linearGradient69056"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-1.243533,-2.588112)"
|
||||
x1="27.243532"
|
||||
y1="54.588112"
|
||||
x2="22.243532"
|
||||
y2="40.588112" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4383"
|
||||
id="linearGradient69709"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(21.756467,-5.588112)"
|
||||
x1="20.243532"
|
||||
y1="37.588112"
|
||||
x2="17.243532"
|
||||
y2="27.588112" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4383"
|
||||
id="linearGradient69717"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(1.2856487,1.4142136)"
|
||||
x1="50.714352"
|
||||
y1="25.585787"
|
||||
x2="48.714352"
|
||||
y2="20.585787" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient73208">
|
||||
<stop
|
||||
style="stop-color:#c4a000;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop73210" />
|
||||
<stop
|
||||
style="stop-color:#edd400;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop73212" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="10"
|
||||
inkscape:cx="8.35"
|
||||
inkscape:cy="27.079831"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:window-height="1104"
|
||||
inkscape:window-x="225"
|
||||
inkscape:window-y="66"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3007"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="0.5"
|
||||
spacingy="0.5" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata3564">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
<dc:title>Path-Stock</dc:title>
|
||||
<dc:date>2015-07-04</dc:date>
|
||||
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
|
||||
<dc:publisher>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:identifier>FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg</dc:identifier>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD LGPL2+</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
|
||||
<dc:contributor>
|
||||
<cc:Agent>
|
||||
<dc:title>[agryson] Alexander Gryson</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:contributor>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
style="display:inline">
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69052"
|
||||
d="m 9,35 0,-14 14,5 0,14 z"
|
||||
style="fill:url(#linearGradient4389);fill-opacity:1.0;fill-rule:nonzero;stroke:#0b1521;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
style="fill:url(#linearGradient69056);fill-opacity:1.0;fill-rule:nonzero;stroke:#0b1521;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 9,49 9,35 37,45 37,59 Z"
|
||||
id="path4381"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4399);fill-opacity:1.0;fill-rule:nonzero;stroke:#0b1521;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 37,59 37,45 55,28 55,41 Z"
|
||||
id="path4391"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#fce94f;fill-opacity:1;fill-rule:nonzero;stroke:#0b1521;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 9,21 29,5 42,10 23,26 Z"
|
||||
id="path4403"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#fce94f;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 11.008035,47.60627 11,38 l 24,8 0.0081,10.184812 z"
|
||||
id="path4381-7"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#edd400;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 39.005041,54.16825 39,46 53,33 l 0.0021,7.176847 z"
|
||||
id="path4391-0"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69038"
|
||||
d="M 23,40 42,23 55,28 37,45 Z"
|
||||
style="fill:#fce94f;fill-opacity:1;fill-rule:nonzero;stroke:#0b1521;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69040"
|
||||
d="m 23,40 0,-14 8,-7 0,14 z"
|
||||
style="fill:url(#linearGradient69042);fill-opacity:1.0;fill-rule:nonzero;stroke:#0b1521;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69044"
|
||||
d="m 25,36 0,-9 4,-3.5 0,9 z"
|
||||
style="fill:none;stroke:#edd400;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69048"
|
||||
d="M 11.008,33.60627 11,24 l 10,3 0,10 z"
|
||||
style="fill:none;stroke:#fce94f;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:url(#linearGradient69709);fill-opacity:1.0;fill-rule:nonzero;stroke:#302b00;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 31,33 0,-14 13,5 0,14.5 z"
|
||||
id="path69705"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#729fcf;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 33,32 0,-10 9,3 0,10.5 z"
|
||||
id="path69707"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69715"
|
||||
d="M 44,38.5 44,24 55,15 55,28 Z"
|
||||
style="fill:url(#linearGradient69717);fill-opacity:1.0;fill-rule:nonzero;stroke:#302b00;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69719"
|
||||
d="m 46,34 0,-9 7,-6 0,8 z"
|
||||
style="fill:none;stroke:#729fcf;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69721"
|
||||
d="m 31,19 11,-9 13,5 -11,9 z"
|
||||
style="fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#302b00;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="temporal"
|
||||
style="display:none;opacity:0.58800001"
|
||||
sodipodi:insensitive="true">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path68967"
|
||||
d="M 9,35 9,49"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path68971"
|
||||
d="M 9,35 37,45"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 55,28 0,13"
|
||||
id="path68973"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 37,45 55,28"
|
||||
id="path68977"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path68983"
|
||||
d="M 23,40 23,26"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path68985"
|
||||
d="m 29,5 13,5"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 23,26 42,10"
|
||||
id="path68989"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 19,13 29,5"
|
||||
id="path68993"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 55,15 -9,8"
|
||||
id="path68997"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69030"
|
||||
d="M 42,23 42,10"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 42,23 14,5"
|
||||
id="path69034"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69036"
|
||||
d="M 23,40 42,23"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path69711"
|
||||
d="m 23,10 19,0"
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#ef2929;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 34,17 0,13"
|
||||
id="path69713"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,84 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
sodipodi:docname="Cfd_workbench_icon.svg"
|
||||
inkscape:version="1.0beta2 (2b71d25, 2019-12-03)"
|
||||
version="1.1"
|
||||
id="svg2816"
|
||||
height="64px"
|
||||
width="64px">
|
||||
<defs
|
||||
id="defs2818" />
|
||||
<sodipodi:namedview
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="61"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-height="1052"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
inkscape:current-layer="layer8"
|
||||
inkscape:cy="34.077045"
|
||||
inkscape:cx="30.558985"
|
||||
inkscape:zoom="10.825726"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base" />
|
||||
<metadata
|
||||
id="metadata2821">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 2"
|
||||
id="layer8"
|
||||
inkscape:groupmode="layer">
|
||||
<path
|
||||
sodipodi:nodetypes="cscsc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3657"
|
||||
d="M 61.095169,40.506926 C 39.817023,35.498558 36.885357,34.950109 17.577376,30.137198 -9.2686372,23.445279 13.357079,17.237473 11.337223,18.144965 c -3.2085292,0.389803 4.901242,-1.121067 9.635335,-0.398369 6.282158,0.959024 40.322493,22.708263 40.322493,22.708263"
|
||||
style="display:inline;fill:#00ff00;fill-rule:evenodd;stroke:#000000;stroke-width:1.75748;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3667"
|
||||
d="M 6.7552618,34.820785 C 31.016687,42.543302 36.822807,41.477249 54.227408,47.648859"
|
||||
style="display:inline;fill:none;fill-rule:evenodd;stroke:#0000ee;stroke-width:2.13543;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3671"
|
||||
d="m 49.568669,42.379789 4.658739,5.26907 v 0 l -4.991506,4.630397"
|
||||
style="display:inline;fill:none;fill-rule:evenodd;stroke:#0000ee;stroke-width:1.96535;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3667-3"
|
||||
d="M 3.3627108,15.373641 C 12.801608,7.8011794 33.065789,12.824069 56.2526,24.488284"
|
||||
style="display:inline;fill:none;fill-rule:evenodd;stroke:#ef0000;stroke-width:2.13543;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3671-6"
|
||||
d="m 52.78476,18.746343 5.329732,5.379334 v 0 l -5.710427,4.727294"
|
||||
style="display:inline;fill:none;fill-rule:evenodd;stroke:#ef0000;stroke-width:1.87465;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 37 KiB |
@@ -1,270 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
id="svg2869"
|
||||
version="1.1"
|
||||
viewBox="0 0 64 64"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs2871">
|
||||
<linearGradient
|
||||
id="linearGradient29">
|
||||
<stop
|
||||
style="stop-color:#204a87;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop29" />
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop30" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient24">
|
||||
<stop
|
||||
id="stop23"
|
||||
offset="0"
|
||||
style="stop-color:#204a87;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop24"
|
||||
offset="1"
|
||||
style="stop-color:#729fcf;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient22">
|
||||
<stop
|
||||
id="stop21"
|
||||
offset="0"
|
||||
style="stop-color:#4e9a06;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop22"
|
||||
offset="1"
|
||||
style="stop-color:#8ae234;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient5">
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop19" />
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop20" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch18">
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop18" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch15">
|
||||
<stop
|
||||
style="stop-color:#3d0000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop15" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient5-1">
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5" />
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3836-9">
|
||||
<stop
|
||||
style="stop-color:#a40000;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3838-8" />
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3840-1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
xlink:href="#linearGradient11"
|
||||
id="radialGradient13"
|
||||
cx="39.502319"
|
||||
cy="40.726604"
|
||||
fx="39.502319"
|
||||
fy="40.726604"
|
||||
r="22.062311"
|
||||
gradientTransform="matrix(1.7937293,1.017333,-1.0383284,1.824402,12.77456,-63.360877)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient11">
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop13" />
|
||||
<stop
|
||||
style="stop-color:#204a87;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop11" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3377-3">
|
||||
<stop
|
||||
id="stop3379-8"
|
||||
offset="0"
|
||||
style="stop-color:#faff2b;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3381-3"
|
||||
offset="1"
|
||||
style="stop-color:#ffaa00;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient4387">
|
||||
<stop
|
||||
style="stop-color:#71b2f8;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4389" />
|
||||
<stop
|
||||
style="stop-color:#002795;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop4391" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3836-0-6-92-4-0">
|
||||
<stop
|
||||
style="stop-color:#a40000;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3838-2-7-06-8-6" />
|
||||
<stop
|
||||
style="stop-color:#ef2929;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3840-5-5-8-7-2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient22"
|
||||
id="linearGradient4516"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="14.577716"
|
||||
y1="27.643585"
|
||||
x2="50.611004"
|
||||
y2="42.347919"
|
||||
gradientTransform="matrix(0.91764667,0,0,0.91543299,7.4569649,-10.851524)" />
|
||||
<linearGradient
|
||||
id="linearGradient3354">
|
||||
<stop
|
||||
id="stop3356"
|
||||
offset="0"
|
||||
style="stop-color:#729fcf;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop3358"
|
||||
offset="1"
|
||||
style="stop-color:#204a87;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="31.92013"
|
||||
x2="53.693264"
|
||||
y1="45.023232"
|
||||
x1="28.158932"
|
||||
gradientTransform="matrix(0.91764667,0,0,0.91543299,5.5967063,-9.6079085)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient18"
|
||||
xlink:href="#linearGradient24" />
|
||||
<linearGradient
|
||||
gradientTransform="matrix(0.91764667,0,0,0.91543299,-34.434605,-11.375877)"
|
||||
y2="34.816742"
|
||||
x2="41.556858"
|
||||
y1="47.53363"
|
||||
x1="65.435036"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient14"
|
||||
xlink:href="#linearGradient3354" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient29"
|
||||
id="linearGradient30"
|
||||
x1="29.777794"
|
||||
y1="35.716694"
|
||||
x2="55.384754"
|
||||
y2="14.091692"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata2874">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>[maxwxyz]</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
|
||||
<dc:publisher>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:identifier>FreeCAD/src/</dc:identifier>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD LGPL2+</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:date>2024</dc:date>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer3"
|
||||
style="display:inline">
|
||||
<path
|
||||
d="M 18.260795,61.000176 C 1.7864687,47.327689 1.5712439,37.276581 7.4738156,17.881577 10.514149,14.174495 15.337186,5.0777001 30.812411,3.2439598 11.081982,19.578959 16.471245,36.612767 22.896918,45.84763 c -4.058659,4.167818 -6.324466,8.717398 -4.636123,15.152546 z"
|
||||
style="opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:evenodd;stroke:#0c1522;stroke-width:2.00001;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path12" />
|
||||
<path
|
||||
id="path16"
|
||||
style="display:inline;opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:evenodd;stroke:#0c1522;stroke-width:2.00001;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 39.80712,40.886165 C 39.347502,18.750404 43.027643,14.637758 50.918334,7.7286519 54.736143,9.018553 59.074083,12.861105 60.054063,15.930397 53.803846,23.917301 52.515998,35.730898 57.174803,58.979264 54.485196,50.098341 51.074626,40.861606 39.80712,40.886165 Z" />
|
||||
<path
|
||||
id="path28"
|
||||
style="display:inline;opacity:1;fill:url(#linearGradient30);fill-opacity:1;fill-rule:evenodd;stroke:#729fcf;stroke-width:2.00001;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 51.318359,10.046875 c 2.664611,1.226923 5.342254,3.630329 6.441407,5.619141 -4.704254,6.607763 -6.335888,15.411672 -4.847657,29.220703 -2.549632,-3.157876 -6.050466,-5.445581 -11.125,-5.908203 -0.197725,-18.706008 2.787826,-22.928537 9.53125,-28.931641 z" />
|
||||
<path
|
||||
id="path24"
|
||||
style="display:inline;opacity:1;fill:#8ae234;fill-opacity:1;fill-rule:evenodd;stroke:#17230b;stroke-width:2.00001;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 22.896918,45.84763 C 10.507844,27.298201 19.418369,12.723127 30.812411,3.2439598 38.009021,2.2698045 45.343419,4.3228707 50.918334,7.7286519 40.938023,15.97348 39.70122,22.647624 39.80712,40.886165 34.254637,41.133337 27.673519,41.988373 22.896918,45.84763 Z" />
|
||||
<path
|
||||
id="path21"
|
||||
style="opacity:1;fill:url(#linearGradient4516);fill-opacity:1;fill-rule:evenodd;stroke:#8ae234;stroke-width:1.93455;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 34.685547,5.0859375 c 4.3141,0.061386 8.542613,1.2198975 12.195312,3.0117188 C 38.840841,15.580985 37.559778,22.755428 37.558594,37.972656 33.070552,38.2911 27.950191,39.15602 23.683594,41.839844 14.12913,25.966693 21.656459,13.68295 31.611328,5.2363281 32.632653,5.1230644 33.659554,5.0713386 34.685547,5.0859375 Z"
|
||||
transform="matrix(1.0338547,0,0,1.0338147,-1.0247137,-0.25384716)" />
|
||||
<path
|
||||
d="M 24.462891,6.7109375 C 11.291828,21.044915 15.087935,35.534635 20.701172,44.390625 18.167597,47.28992 16.422634,50.565513 16.210937,54.5 4.8313443,43.641922 4.991564,34.921511 9.9746094,18.4375 12.780553,14.921062 16.016896,9.4755874 24.462891,6.7109375 Z"
|
||||
style="opacity:1;fill:url(#linearGradient14);fill-opacity:1;fill-rule:evenodd;stroke:#729fcf;stroke-width:1.93455;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path26"
|
||||
transform="matrix(1.0338547,0,0,1.0338147,-1.0247137,-0.25384716)" />
|
||||
<path
|
||||
style="fill:none;stroke:#0c1522;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 8.4851839,51.269783 C 11.811877,31.098625 44.97231,22.579841 54.52943,40.463301"
|
||||
id="path839" />
|
||||
<path
|
||||
style="fill:none;stroke:#0c1522;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 4.0012601,37.976551 C 14.369364,18.537691 43.536681,16.009382 54.707116,29.842375"
|
||||
id="path841" />
|
||||
<path
|
||||
style="fill:none;stroke:#0c1522;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 5.5280638,24.90186 C 20.261449,10.062113 39.319651,5.3312343 56.764105,21.582244"
|
||||
id="path843" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,197 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933312 16.933312"
|
||||
version="1.1"
|
||||
id="svg50"
|
||||
sodipodi:docname="DesignSPHysics_workbench_icon_new_64x64_2.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<metadata
|
||||
id="metadata54">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1853"
|
||||
inkscape:window-height="1019"
|
||||
id="namedview52"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.6074562"
|
||||
inkscape:cx="-45.759252"
|
||||
inkscape:cy="61.417337"
|
||||
inkscape:window-x="1433"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg50" />
|
||||
<defs
|
||||
id="defs32">
|
||||
<linearGradient
|
||||
id="linearGradient1091"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
id="stop1087"
|
||||
offset="0"
|
||||
style="stop-color:#de2230;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop1089"
|
||||
offset="1"
|
||||
style="stop-color:#2b6ab0;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1075">
|
||||
<stop
|
||||
style="stop-color:#983176;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1071" />
|
||||
<stop
|
||||
style="stop-color:#2b6ab0;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop1073" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="A"
|
||||
cx="282.64999"
|
||||
cy="29.149"
|
||||
r="19.570999"
|
||||
gradientTransform="matrix(0.61866,0.96665,-1.0332,0.66128,-327.28,-255.84)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#71b2f8"
|
||||
offset="0"
|
||||
id="stop2" />
|
||||
<stop
|
||||
stop-color="#002795"
|
||||
offset="1"
|
||||
id="stop4" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="B"
|
||||
cx="270.57999"
|
||||
cy="33.900002"
|
||||
r="19.570999"
|
||||
gradientTransform="matrix(1.1149,0.27223,-0.75072,3.0746,-471.09,-148.33)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#ff6d0f"
|
||||
offset="0"
|
||||
id="stop7" />
|
||||
<stop
|
||||
stop-color="#ff1000"
|
||||
offset="1"
|
||||
id="stop9" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="C"
|
||||
x1="142.42599"
|
||||
x2="226.73"
|
||||
y1="162.099"
|
||||
y2="162.099"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.06151372,0,0,0.06151372,12.90318,-2.121192)">
|
||||
<stop
|
||||
stop-color="#666"
|
||||
stop-opacity=".847"
|
||||
offset="0"
|
||||
id="stop12" />
|
||||
<stop
|
||||
stop-color="#666"
|
||||
stop-opacity=".25"
|
||||
offset=".518"
|
||||
id="stop14" />
|
||||
<stop
|
||||
stop-color="#666"
|
||||
stop-opacity=".025"
|
||||
offset=".776"
|
||||
id="stop16" />
|
||||
<stop
|
||||
stop-color="#666"
|
||||
stop-opacity="0"
|
||||
offset="1"
|
||||
id="stop18" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="D"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
flood-color="#000"
|
||||
flood-opacity=".498"
|
||||
id="feFlood21" />
|
||||
<feComposite
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
id="feComposite23" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="2.5"
|
||||
id="feGaussianBlur25" />
|
||||
<feOffset
|
||||
dx="0.3"
|
||||
dy="0.8"
|
||||
id="feOffset27"
|
||||
result="result1" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="result1"
|
||||
id="feComposite29" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1075"
|
||||
id="linearGradient1077"
|
||||
x1="12.879064"
|
||||
y1="190.00041"
|
||||
x2="127.98798"
|
||||
y2="78.355347"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1091"
|
||||
id="linearGradient1085"
|
||||
x1="-20.224503"
|
||||
y1="109.90421"
|
||||
x2="53.218998"
|
||||
y2="155.82506"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
id="g1069"
|
||||
transform="matrix(0.10285649,0,0,0.10267995,1.0534131,-5.6318672)"
|
||||
style="stroke-width:0.59856778">
|
||||
<path
|
||||
sodipodi:nodetypes="csczcc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1061"
|
||||
d="m 19.243778,189.86421 c 0,0 15.84417,-22.71955 9.352293,-54.45045 -5.996226,-29.30827 -20.8817476,-29.27105 -20.8817476,-29.27105 0,0 6.3886856,-26.330422 22.6624956,-19.823753 16.273809,6.506669 40.275125,77.872223 49.12365,119.176843 z"
|
||||
style="fill:url(#linearGradient1085);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5745616;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccscsc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path871"
|
||||
d="m 33.57962,193.44157 73.18635,19.80337 c 0,0 -28.276859,-25.17999 -14.063259,-84.66656 7.749139,-32.431605 47.355879,-46.20786 47.355879,-46.20786 0,0 -25.23199,-34.985128 -48.419841,-13.860317 C 49.862564,106.56952 33.57962,193.44157 33.57962,193.44157 Z"
|
||||
style="fill:url(#linearGradient1077);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5745616;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -1,90 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="DynamicDataSVGLogo.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.979899"
|
||||
inkscape:cx="-81.428571"
|
||||
inkscape:cy="68.194187"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1001"
|
||||
inkscape:window-x="1791"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-280.06665)">
|
||||
<image
|
||||
y="280.21786"
|
||||
x="0.60476506"
|
||||
id="image4537"
|
||||
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAA3NCSVQICAjb4U/gAAAEmUlEQVRo
|
||||
ge1az2vjRhiVvT0VSi67IOjKgeaQxRPQMZHZQntZJ9eS0R4CNbS3diFkDblapodCim32D0hLiy8Z
|
||||
5bQ9+VSoidWEUFiQHJI2FNkpCBwIXkKPcQ+yFc9oRh4pcWyB32kszzfznma+HzN2otfrCXFGctIE
|
||||
7oqZgEnjg2hmmqa5jbm5uTsy6Ha7xJihEPsVmAmYNBLR8kClUnEbqVTqjgxarZbbSCYpb/Pm5sZr
|
||||
ZzIZt7G8vOw9jP0KzARMGhHzwDiwuLgY3GFhYcH/MPYrMBMwaWA+cHh46DYajYb3kBqex4HV1dUI
|
||||
VrFfgZmASQPzAS/QXl1dBZudnJyMi1FIxH4FsGr08vLSbRwfHwebra2t0b9Q8vt5JRQDrxp99Kjz
|
||||
548//fzOoXSCqIcg1TzKCrx4v5uTxQiGwXgl/kVnHwjMB7zie+QhIcJMY8J0+UCEhY0ioPbRV+PY
|
||||
Qm/+zdx1C/FDzm2XBUEQhFTij60t1I42ig/lsjuqsPXifWpJ4xkWE3B+fu42zs7OvIeJRIKwGdN1
|
||||
ajKZHD4B81qNg8pDIvYCpuhIubm5efvBKnL6FSZAUfpJdGVlxXvo3/HJZNK7FxIE4b48OBpiv4Wm
|
||||
SoAVwSaKACLYSRGGoAMwP7CB+cDBwYHbODo68h4+2Jk4GqaaHA9mAiYNzAe828nr62u3Yder1d/r
|
||||
rZZt24NO8/PzQurZs0/kbPSKtG0g1DAuLtptL4mI4u6uASFcL0BO96UIGIJdr1ar1bpN+ca2Bdu2
|
||||
67WaIIryxsaIG1k/c4QMSu5zHMvRLUvXNAEAWCjwqqCdiU/ffrHxNY07BwLOxG2jVC7RuHOAfSb2
|
||||
rcDpD8+f75xGmiUIbXSPx4ZhYAIeP/5NfUKyBwAW9goQ9NfUsvT9YlHTQ2VNo+RjL0mK+lpVJMm9
|
||||
lXCcdx/+cxxyWEEgopCuqjpOHiLTNJHHXnD1ILO8nZND0C8ZOHklX6lU8op0m8RFUS4gs2cyNgob
|
||||
QwJI+kAzTcQKCaKcK/NpIOlLaqWSVxj1B2BvdgZuBeg6QX9vVCSQcxwSjAZB/7U6oniCKIwETwDB
|
||||
ny+QydlRbAj+ijrKwJ1c484FAwEkf8j3EkSFtRlckPwzfNeOYJ07m9FLCQDSnPaS9JSzZ6jOIM1L
|
||||
gC4gneZewqcS/3kgRN804GRAF9BscsfjizZ/egrRt2lxMhgIwBVbVpNzonb7Iuhr/J2P6DxMoMlL
|
||||
YCCA2HSETzPhGMHFDbHrCZ9mwtrnTsneFiLiji8p0+DUfhlV3xBxx5eUabCKLzXuLXzrA2Tw1dUl
|
||||
lfkeut3u32+/36n5LpP/u2zh+PjTLHZsMEqvvvv1aKhDd4AB+6UlfvpEOU0zBlArFNaHqiHLsprF
|
||||
z7/VO53AgUU592X/Et6p7fikinI2m5VlURz86a/TgZ91Xhb1Ec4LINrD65seDmbxw4CczTLPZXKu
|
||||
PEDY3xOgxs7FEA0TJsMoRKbJm8efpOE3nEWpnNveZivFASAy0TpfX8G3An3gJTR1Fs10ezLkAojM
|
||||
hxiWIaA/n4k0CAE2JwBQo1ALg3sdNuK/FqcHsb8X+h/s/sbLpcJe1gAAAABJRU5ErkJggg==
|
||||
"
|
||||
style="image-rendering:optimizeQuality"
|
||||
preserveAspectRatio="none"
|
||||
height="16.933332"
|
||||
width="16.933332" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,602 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64px"
|
||||
height="64px"
|
||||
id="svg2816"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="EMWorkbench.svg"
|
||||
inkscape:export-filename="C:\Users\ediloren\AppData\Roaming\FreeCAD\Mod\EM\Resources\EMWorkbench.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs2818">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4289">
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4291" />
|
||||
<stop
|
||||
style="stop-color:#204a87;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4293" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient4248">
|
||||
<stop
|
||||
id="stop4250"
|
||||
offset="0"
|
||||
style="stop-color:#a40000;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop4252"
|
||||
offset="1"
|
||||
style="stop-color:#ef2929;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3789">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3791" />
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3793" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3781">
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3783" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3785" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 32 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="64 : 32 : 1"
|
||||
inkscape:persp3d-origin="32 : 21.333333 : 1"
|
||||
id="perspective2824" />
|
||||
<inkscape:perspective
|
||||
id="perspective3622"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3622-9"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3653"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3675"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3697"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3720"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3742"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3764"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3785"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3806"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3806-3"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective3835"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3781"
|
||||
id="linearGradient3787"
|
||||
x1="93.501396"
|
||||
y1="-0.52792466"
|
||||
x2="92.882462"
|
||||
y2="-7.2011309"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3789"
|
||||
id="linearGradient3795"
|
||||
x1="140.23918"
|
||||
y1="124.16501"
|
||||
x2="137.60997"
|
||||
y2="117.06711"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3781-6"
|
||||
id="linearGradient3804-3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="93.501396"
|
||||
y1="-0.52792466"
|
||||
x2="92.882462"
|
||||
y2="-7.2011309" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3781-6">
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3783-7" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3785-5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3789-5"
|
||||
id="linearGradient3806-3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="140.23918"
|
||||
y1="124.16501"
|
||||
x2="137.60997"
|
||||
y2="117.06711" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3789-5">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3791-6" />
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3793-2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3781-0"
|
||||
id="linearGradient3804-36"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="93.501396"
|
||||
y1="-0.52792466"
|
||||
x2="92.882462"
|
||||
y2="-7.2011309" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3781-0">
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3783-6" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3785-2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3789-1"
|
||||
id="linearGradient3806-6"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="140.23918"
|
||||
y1="124.16501"
|
||||
x2="137.60997"
|
||||
y2="117.06711" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3789-1">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3791-8" />
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3793-7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3781-8"
|
||||
id="linearGradient3804-2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="93.501396"
|
||||
y1="-0.52792466"
|
||||
x2="92.814743"
|
||||
y2="-5.3353744" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3781-8">
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3783-9" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3785-7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3789-12"
|
||||
id="linearGradient3806-36"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="140.23918"
|
||||
y1="124.16501"
|
||||
x2="137.60997"
|
||||
y2="117.06711" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3789-12">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3791-9" />
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3793-3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3781-03"
|
||||
id="linearGradient3804-5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="93.501396"
|
||||
y1="-0.52792466"
|
||||
x2="92.814743"
|
||||
y2="-5.3353744" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3781-03">
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3783-61" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3785-0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3789-2"
|
||||
id="linearGradient3806-63"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="140.23918"
|
||||
y1="124.16501"
|
||||
x2="137.60997"
|
||||
y2="117.06711" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3789-2">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3791-0" />
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3793-6" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="18.0625"
|
||||
fy="41.625"
|
||||
fx="25.1875"
|
||||
cy="41.625"
|
||||
cx="25.1875"
|
||||
gradientTransform="matrix(1,0,0,0.32526,0,28.08607)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient3169"
|
||||
xlink:href="#linearGradient2269-0"
|
||||
inkscape:collect="always" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2269-0">
|
||||
<stop
|
||||
offset="0"
|
||||
id="stop2271-4"
|
||||
style="stop-color:#000000;stop-opacity:1;" />
|
||||
<stop
|
||||
offset="1"
|
||||
id="stop2273-87"
|
||||
style="stop-color:#000000;stop-opacity:0;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="matrix(0.49538883,0,0,0.49063187,2.3277017,31.102625)"
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="linearGradient3606"
|
||||
x1="51.037281"
|
||||
y1="47.692612"
|
||||
x2="14.872466"
|
||||
y2="10.397644"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3600">
|
||||
<stop
|
||||
style="stop-color:#2e3436;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3602" />
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3604" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="matrix(0.49538883,0,0,0.49063187,30.327701,3.102625)"
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4248"
|
||||
id="linearGradient3606-1"
|
||||
x1="51.037281"
|
||||
y1="47.692612"
|
||||
x2="14.872466"
|
||||
y2="10.397644"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3873"
|
||||
id="linearGradient4043"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.87502198,0,0,0.34687969,-94.40985,36.126007)"
|
||||
x1="126.79221"
|
||||
y1="22.888617"
|
||||
x2="131.8111"
|
||||
y2="34.041721" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3873">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3875" />
|
||||
<stop
|
||||
style="stop-color:#d3d7cf;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3877" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3873"
|
||||
id="linearGradient4221"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.91737886,0,0,0.22453536,-71.851019,11.452429)"
|
||||
x1="127.49857"
|
||||
y1="-0.16112821"
|
||||
x2="129.43178"
|
||||
y2="57.091461" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4289"
|
||||
id="linearGradient4295"
|
||||
x1="10.527967"
|
||||
y1="36.787956"
|
||||
x2="26.408852"
|
||||
y2="53.650681"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="6.404658"
|
||||
inkscape:cx="-27.433748"
|
||||
inkscape:cy="16.593673"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:snap-bbox="false"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1018"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true"
|
||||
borderlayer="true"
|
||||
gridtolerance="10000"
|
||||
inkscape:snap-perpendicular="false"
|
||||
inkscape:snap-tangential="false"
|
||||
inkscape:snap-grids="true"
|
||||
inkscape:snap-nodes="false"
|
||||
inkscape:snap-others="false"
|
||||
inkscape:snap-to-guides="false">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3005"
|
||||
empspacing="2"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="false"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
dotted="false" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata2821">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>[triplus]</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:title>ArchWorkbench</dc:title>
|
||||
<dc:date>2016-02-26</dc:date>
|
||||
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
|
||||
<dc:publisher>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:identifier>FreeCAD/src/Mod/Arch/Resources/icons/ArchWorkbench.svg</dc:identifier>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD LGPL2+</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
|
||||
<dc:contributor>
|
||||
<cc:Agent>
|
||||
<dc:title>[agryson] Alexander Gryson</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:contributor>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer">
|
||||
<ellipse
|
||||
cy="46"
|
||||
cx="18"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient3606);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.91892004;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
id="path2826"
|
||||
rx="12.970181"
|
||||
ry="12.845634" />
|
||||
<ellipse
|
||||
cy="46"
|
||||
cx="18"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient4295);stroke:#729fcf;stroke-width:1.865;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate;fill-opacity:1"
|
||||
id="path2826-4"
|
||||
rx="11.067488"
|
||||
ry="11.067487" />
|
||||
<ellipse
|
||||
cy="18"
|
||||
cx="46"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient3606-1);fill-opacity:1;fill-rule:evenodd;stroke:#280000;stroke-width:1.91892004;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
id="path2826-5"
|
||||
rx="12.970181"
|
||||
ry="12.845634" />
|
||||
<ellipse
|
||||
cy="17.999998"
|
||||
cx="46"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#ef2929;stroke-width:1.86502409;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:accumulate"
|
||||
id="path2826-4-9"
|
||||
rx="11.067488"
|
||||
ry="11.067487" />
|
||||
<rect
|
||||
style="fill:url(#linearGradient4043);fill-opacity:1;stroke:#0b1521;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect3871"
|
||||
width="12.686974"
|
||||
height="4.2556629"
|
||||
x="11.656513"
|
||||
y="43.872169" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4221);fill-opacity:1;stroke:#280000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 39.349662,19.221209 0,-2.754691 5.320444,0 0,-5.509383 2.66022,0 0,5.509383 5.320443,0 0,2.754691 -5.320443,0 0,5.509385 -2.66022,0 0,-5.509385 z"
|
||||
id="rect3871-3"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
|
||||
d="M 0,-0.01590846 64.328181,64.156136"
|
||||
id="path4297"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-dashoffset:0"
|
||||
d="M 0.15613636,19.969546 C 10.513182,21.53091 22.030268,24.831738 29.822046,33.241137 c 8.794633,7.685029 12.855226,20.609999 14.364544,30.44659"
|
||||
id="path4299"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
|
||||
d="M 0.15613636,27.9325 C 18.779553,28.17068 23.615656,30.723314 28.572955,35.114773 33.413181,40.111137 36.072347,46.132588 36.223636,64"
|
||||
id="path4301"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-dashoffset:0"
|
||||
d="M 63.959135,44.197594 C 53.602089,42.63623 42.085003,39.335402 34.293225,30.926003 25.498592,23.240974 21.437999,10.316004 19.928681,0.47941339"
|
||||
id="path4299-4"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-dashoffset:0"
|
||||
d="M 63.959135,36.23464 C 45.335718,35.99646 40.499615,33.443826 35.542316,29.052367 30.70209,24.056003 28.042924,18.034552 27.891635,0.16714039"
|
||||
id="path4301-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
id="svg4152"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r"
|
||||
viewBox="0 0 64 64"
|
||||
sodipodi:docname="WorkbenchIcon.svg">
|
||||
<defs
|
||||
id="defs4154" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.48613591"
|
||||
inkscape:cx="-271.96604"
|
||||
inkscape:cy="172.36689"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1028"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata4157">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer">
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4239"
|
||||
width="9.256671"
|
||||
height="14.52783"
|
||||
x="-41.460148"
|
||||
y="1.9454339"
|
||||
transform="matrix(0.01127127,-0.99993648,0.99993648,0.01127127,0,0)" />
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4241"
|
||||
width="14.142136"
|
||||
height="7.4567623"
|
||||
x="-32.460606"
|
||||
y="5.545249"
|
||||
transform="matrix(0.01127127,-0.99993648,0.99993648,0.01127127,0,0)" />
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4239-8"
|
||||
width="9.256671"
|
||||
height="14.52783"
|
||||
x="3.7287047"
|
||||
y="46.867786" />
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4241-4"
|
||||
width="13.756441"
|
||||
height="7.4567623"
|
||||
x="12.728239"
|
||||
y="50.467594" />
|
||||
<rect
|
||||
style="opacity:1;fill:#0039ff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4261"
|
||||
width="34.113159"
|
||||
height="38.23526"
|
||||
x="28.497931"
|
||||
y="2.160291" />
|
||||
<circle
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000bff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:3, 1;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4269"
|
||||
cx="51.636368"
|
||||
cy="13.100049"
|
||||
r="5.818182" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ff9800;stroke-width:3.70000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:7.4, 3.7;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 9.1281027,17.330948 0,-4.88546 42.5549703,-0.12857"
|
||||
id="path4265"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ff9800;stroke-width:3.70000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:11.10000014, 3.70000005;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 26.495713,54.4862 26.21588,-0.128565 -0.0377,-13.773797"
|
||||
id="path4267"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="585.918px" height="585.918px" viewBox="0 0 585.918 585.918" style="enable-background:new 0 0 585.918 585.918;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M132.22,274.777c-4.761,0.087-7.822,0.578-9.628,1.056v65.29c1.806,0.503,4.73,0.604,7.387,0.692
|
||||
c19.625,0.813,32.749-10.33,32.749-34.897C162.876,285.549,150.905,274.463,132.22,274.777z"/>
|
||||
<path d="M537.115,48.974h-177.39c-0.787,0-1.553,0.076-2.335,0.123V0L11.176,46.206v492.311l346.214,47.402v-50.583
|
||||
c0.782,0.052,1.543,0.114,2.335,0.114h177.39c20.75,0,37.627-16.888,37.627-37.627V86.602
|
||||
C574.742,65.859,557.865,48.974,537.115,48.974z M51.271,358.419c-10.767-0.536-19.717-4.146-24.458-7.801l3.872-16.205
|
||||
c3.386,2.125,11.223,6.147,19.173,6.42c10.265,0.367,15.533-5.061,15.533-12.304c0-9.461-8.407-13.859-17.086-13.984l-7.93-0.117
|
||||
v-15.789l7.535,0.01c6.596-0.137,15.101-2.929,15.101-11.044c0-5.75-4.203-9.96-12.526-9.827
|
||||
c-6.797,0.108-13.905,3.538-17.302,5.881l-3.876-15.74c4.882-3.714,14.761-7.478,25.588-7.797
|
||||
c18.342-0.557,28.741,10.089,28.741,23.42c0,10.352-5.278,18.437-16.003,22.53v0.294c10.441,2.162,19.003,11.119,19.003,23.886
|
||||
C86.642,347.552,72.967,359.505,51.271,358.419z M170.134,348.089c-10.488,8.597-26.132,12.109-44.762,11.227
|
||||
c-10.9-0.52-18.512-1.615-23.636-2.576v-96.448c7.557-1.527,17.551-2.564,28.248-2.887c18.186-0.559,30.251,2.575,39.848,9.827
|
||||
c10.499,7.805,17.173,20.588,17.173,39.081C187.005,326.351,179.879,339.951,170.134,348.089z M299.285,366.387l-1.785-43.392
|
||||
c-0.536-13.61-1.066-30.061-1.066-46.501l-0.537,0.01c-3.719,14.438-8.656,30.471-13.223,43.567l-14.192,43.072l-20.092-0.914
|
||||
l-11.903-42.551c-3.591-12.746-7.315-28.14-9.927-42.204h-0.307c-0.657,14.546-1.129,31.164-1.93,44.565l-1.932,40.768
|
||||
l-22.282-1.046l6.714-105.911l32.636-0.977l10.932,36.24c3.517,12.701,7.053,26.433,9.597,39.41l0.496,0.011
|
||||
c3.238-12.735,7.157-27.319,10.932-39.673l12.494-37.32l35.665-1.065l6.828,115.192L299.285,366.387z M553.24,497.823
|
||||
c0,8.887-7.238,16.127-16.125,16.127h-177.39c-0.797,0-1.563-0.117-2.335-0.231V324.34c4.729,6.604,9.422,13.689,15.019,21.553
|
||||
c0.966-5.39,2.353-8.347,1.805-10.887c-2.771-13.139-4.828-26.624-9.458-39.124c-4.516-12.181-2.451-19.039,10.12-22.899
|
||||
c8.588-2.639,16.893-6.187,25.312-9.332c-5.568,3.279-11.926,5.72-16.546,10.032c-8.011,7.46-5.663,17.841,4.263,21.625
|
||||
c3.685,1.409,7.728,1.84,11.623,2.721c-0.898,1.202-1.791,2.405-2.688,3.602c7.232,6.384,14.467,12.746,21.679,19.15
|
||||
c12.883,11.423,15.891-10.782,26.31-7.716c-3.348,5.846-7.591,11.369-9.868,17.589c-3.197,8.783-4.62,18.212-17.789,14.789
|
||||
c-19.854-5.149-27.219-12.642-26.517-26.426c0.326-6.446-0.87-12.977-1.385-19.464c-4.378,7.611-4.269,14.761-6.11,21.364
|
||||
c-3.896,13.959,0.986,21.763,13.343,29.322c23.077,14.111,44.7,30.626,66.973,46.06c4.599,3.201,9.439,6.068,14.158,9.07
|
||||
c1.254-0.955,2.498-1.9,3.742-2.866c-1.938-5.06-4.966-10.006-5.553-15.233c-0.683-6.021,0.366-12.378,1.686-18.368
|
||||
c0.246-1.112,6.838-2.577,7.768-1.507c3.974,4.568,7.139,9.9,10.063,15.255c0.729,1.323-0.557,3.749-0.688,4.525
|
||||
c-4.394-2.277-8.666-4.494-16.067-8.346c9.009,20.608,15.117,21.926,25.375,8.702c3.622-4.651,5.386-10.697,8.388-15.891
|
||||
c4.941-8.508,8.359-19.274,15.681-24.738c17.329-12.92,12.861-28.936,11.784-45.618c-15.652,2.792-28.021,18.919-44.44,8.021
|
||||
c6.782-10.944,12.578-20.31,19.821-31.979c-5.921,0-7.895-0.407-9.627,0.071c-37.605,10.428-38.771,10.239-56.852-23.108
|
||||
c-5.217-9.627-10.478-12.703-19.853-5.882c-3.233,2.349-7.002,3.951-10.525,5.893c22.483-24.331,23.086-27.098,9.014-56.131
|
||||
c-3.701-7.639-8.105-15.168-13.439-21.712c-7.328-8.981-15.874-16.974-27.075-28.725c-3.47,18.954-3.906,33.35-8.926,45.922
|
||||
c-5.814,14.583-12.954,28.688-20.519,42.536V70.717c0.771-0.113,1.532-0.242,2.335-0.242h177.39
|
||||
c8.887,0,16.125,7.236,16.125,16.127V497.823z M406.813,303.394c19.476,13.984,25.135,11.333,25.135,11.333
|
||||
c-5.24,5.664-12.997,0-12.997,0L406.813,303.394z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -1,329 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="68px"
|
||||
height="68px"
|
||||
id="svg2816"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="Inventor-WB.svg">
|
||||
<defs
|
||||
id="defs2818">
|
||||
<linearGradient
|
||||
id="linearGradient3602">
|
||||
<stop
|
||||
style="stop-color:#ff2600;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3604" />
|
||||
<stop
|
||||
style="stop-color:#ff5f00;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3606" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 32 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="64 : 32 : 1"
|
||||
inkscape:persp3d-origin="32 : 21.333333 : 1"
|
||||
id="perspective2824" />
|
||||
<inkscape:perspective
|
||||
id="perspective3618"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3602-7"
|
||||
id="linearGradient3608-5"
|
||||
x1="3.909091"
|
||||
y1="14.363636"
|
||||
x2="24.81818"
|
||||
y2="14.363636"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3602-7">
|
||||
<stop
|
||||
style="stop-color:#c51900;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3604-1" />
|
||||
<stop
|
||||
style="stop-color:#ff5f00;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3606-3" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective3677"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3602-5"
|
||||
id="linearGradient3608-1"
|
||||
x1="3.909091"
|
||||
y1="14.363636"
|
||||
x2="24.81818"
|
||||
y2="14.363636"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3602-5">
|
||||
<stop
|
||||
style="stop-color:#c51900;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3604-9" />
|
||||
<stop
|
||||
style="stop-color:#ff5f00;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3606-9" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="14.363636"
|
||||
x2="24.81818"
|
||||
y1="14.363636"
|
||||
x1="3.909091"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient3686"
|
||||
xlink:href="#linearGradient3602-5"
|
||||
inkscape:collect="always" />
|
||||
<inkscape:perspective
|
||||
id="perspective3717"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3602-58"
|
||||
id="linearGradient3608-8"
|
||||
x1="3.909091"
|
||||
y1="14.363636"
|
||||
x2="24.81818"
|
||||
y2="14.363636"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3602-58">
|
||||
<stop
|
||||
style="stop-color:#c51900;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3604-2" />
|
||||
<stop
|
||||
style="stop-color:#ff5f00;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3606-2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="14.363636"
|
||||
x2="24.81818"
|
||||
y1="14.363636"
|
||||
x1="3.909091"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient3726"
|
||||
xlink:href="#linearGradient3602-58"
|
||||
inkscape:collect="always" />
|
||||
<inkscape:perspective
|
||||
id="perspective4410"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective4944"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective4966"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective5009"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective5165"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7581"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7606"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7638"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7660"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7704"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7730"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7762"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7783"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<inkscape:perspective
|
||||
id="perspective7843"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<filter
|
||||
inkscape:collect="always"
|
||||
style="color-interpolation-filters:sRGB"
|
||||
id="filter878"
|
||||
x="-0.054005238"
|
||||
width="1.1080105"
|
||||
y="-0.035997672"
|
||||
height="1.0719953">
|
||||
<feGaussianBlur
|
||||
inkscape:collect="always"
|
||||
stdDeviation="0.94509399"
|
||||
id="feGaussianBlur880" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9999996"
|
||||
inkscape:cx="13.164331"
|
||||
inkscape:cy="35.428063"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
showguides="false" />
|
||||
<metadata
|
||||
id="metadata2821">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
transform="matrix(0.53992752,0,0,0.54612395,15.182416,15.213398)"
|
||||
style="stroke-width:1.84156334">
|
||||
<g
|
||||
id="g3755"
|
||||
transform="translate(-2.1886387,-1.8655814)">
|
||||
<path
|
||||
inkscape:export-ydpi="121.55234"
|
||||
inkscape:export-xdpi="121.55234"
|
||||
inkscape:export-filename="D:\Inventor\InventorLoader\Icon.png"
|
||||
sodipodi:nodetypes="ccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6342"
|
||||
d="m 18.836744,-24.017438 52.79118,-0.683828 V 4.374427 H 49.783297 v 56.431952 h 21.844627 c 0.02649,11.584862 -2.760418,24.960305 -3.640771,27.305783 -4.674513,0.606794 -22.603238,2.018094 -49.160272,1.820385 z"
|
||||
style="opacity:0.98799995;fill:#cd4500;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.35235524px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:export-ydpi="121.55234"
|
||||
inkscape:export-xdpi="121.55234"
|
||||
inkscape:export-filename="D:\Inventor\InventorLoader\Icon.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="polygon49"
|
||||
d="m -3.0078826,4.374427 7.281542,-29.126169 H 18.836744 L 49.783297,4.374427 Z"
|
||||
style="opacity:0.8;fill:#ff6000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.35235524px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:export-ydpi="121.55234"
|
||||
inkscape:export-xdpi="121.55234"
|
||||
inkscape:export-filename="D:\Inventor\InventorLoader\Icon.png"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="polygon51"
|
||||
d="M -4.8282686,60.806379 H 49.783297 L 18.826881,89.932547 H -4.8282686 Z"
|
||||
style="opacity:0.8;fill:#ff6000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.35235524px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
transform="matrix(1.8203855,0,0,1.8203855,44.820058,23.99752)"
|
||||
inkscape:export-ydpi="121.55234"
|
||||
inkscape:export-xdpi="121.55234"
|
||||
inkscape:export-filename="D:\Inventor\InventorLoader\Icon.png"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path58"
|
||||
d="m 16.726477,-24.752292 -2,0.02539 v 13.947265 H 2.7264767 v 31 h 2 V -8.7796363 H 16.726477 Z m -40.5,13.972656 -0.5,1.9999997 h 9.998047 v -1.9999997 z m 38.449219,33 c -0.248643,5.820848 -1.515612,11.844764 -1.949219,13 -2.56787,0.333333 -12.41717232,1.108608 -27.005859,1 h -10.994141 v 2 h 12.994141 c 14.5886867,0.108608 24.437989,-0.666667 27.005859,-1 0.483608,-1.288451 2.01455,-8.636039 2,-15 z"
|
||||
style="opacity:0.22099998;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.84156334px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter878)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="small_icon.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.2"
|
||||
inkscape:cx="32.49008"
|
||||
inkscape:cy="36.240377"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid850" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-280.06663)">
|
||||
<path
|
||||
style="fill:#666666;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 16.66875,280.33122 v 15.875 H 9.7895829 v -2.64584 H 11.90625 V 290.1208 H 9.7895829 v -3.70417 H 11.90625 v -3.175 H 9.7895829 v -2.91041 z"
|
||||
id="path4555-1"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
<path
|
||||
style="fill:#666666;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 1.3229167,280.33121 v 15.875 H 7.408334 v -2.91042 h 1.8520833 l -5e-7,-2.91042 H 7.408334 v -4.23333 h 1.8520833 v -2.64583 H 7.408334 v -3.175 z"
|
||||
id="path4555-4-0"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
<path
|
||||
style="fill:#005bff;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 15.875001,281.12496 0,15.61042 H 8.9958335 l 10e-8,-2.38126 h 2.1166674 v -3.43958 H 8.9958336 v -3.70417 h 2.1166674 v -3.175 H 8.9958336 v -2.91041 z"
|
||||
id="path4555"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
<path
|
||||
style="fill:#ff2124;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 0.52916686,281.12496 3.4e-7,15.61042 h 6.0854167 v -2.64584 h 1.8520834 l -4e-7,-2.91042 h -1.852083 v -4.23333 h 1.8520834 v -2.64583 H 6.6145839 v -3.175 z"
|
||||
id="path4555-4"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,117 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.1 r15371"
|
||||
sodipodi:docname="Workbench.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4536">
|
||||
<stop
|
||||
style="stop-color:#75507b;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4532" />
|
||||
<stop
|
||||
style="stop-color:#ad7fa8;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4534" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4536"
|
||||
id="linearGradient4538"
|
||||
x1="15.345834"
|
||||
y1="294.88333"
|
||||
x2="1.5875"
|
||||
y2="282.18332"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6"
|
||||
inkscape:cx="41.809024"
|
||||
inkscape:cy="32.872782"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4485"
|
||||
empspacing="2"
|
||||
empcolor="#3f3fff"
|
||||
empopacity="0.28627451"
|
||||
color="#3f3fff"
|
||||
opacity="0.09411765" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-280.06665)">
|
||||
<path
|
||||
style="fill:none;stroke:#171018;stroke-width:0.5291667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="m 0.79375,295.67707 4.9e-7,-14.2875 H 16.139583 v 14.2875 z"
|
||||
id="path4487"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4538);stroke:#ad7fa8;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 1.3229166,295.1479 V 281.91873 H 15.610416 v 13.22917 z"
|
||||
id="path4530"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#ad7fa8;fill-opacity:1;stroke:#171018;stroke-width:0.52916664;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4542"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="3.96875"
|
||||
sodipodi:cy="284.56458"
|
||||
sodipodi:rx="1.5875"
|
||||
sodipodi:ry="1.5875"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="6.2766054"
|
||||
sodipodi:open="true"
|
||||
d="m 5.55625,284.56458 a 1.5875,1.5875 0 0 1 -1.5848886,1.58749 1.5875,1.5875 0 0 1 -1.5901028,-1.58227 1.5875,1.5875 0 0 1 1.5796572,-1.59271 1.5875,1.5875 0 0 1 1.5952998,1.57704" />
|
||||
<path
|
||||
id="path4546"
|
||||
style="fill:none;stroke:#171018;stroke-width:0.52916667;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 6.6145832,292.50207 5.2916668,-5.29167 2.645833,2.64583 m -12.170833,2.64584 4.2333332,-4.23334 2.1166667,2.11667" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |