def setUp(self): """ Prepare unit test. """ super(TestSetupProjectWizard, self).setUp( parameters={"primary_root_name": "primary"} ) self._wizard = sgtk.get_command("setup_project_factory").execute({}) self._storage_locations = ShotgunPath( "Z:\\projects", "/mnt/projects", "/Volumes/projects" ) self._storage_locations.current_os = self.tank_temp self.mockgun.update( "LocalStorage", self.primary_storage["id"], self._storage_locations.as_shotgun_dict(), ) # Prepare the wizard for business. All these methods are actually passing # information directly to the SetupProjectParams object inside # the wizard, so there's no need to test them per-se. self._wizard.set_project(self.project["id"], force=True) self._wizard.set_use_distributed_mode() self.config_uri = os.path.join(self.fixtures_root, "config") self._wizard.set_config_uri(self.config_uri)
def test_otls_installed(self): """ Checks that the otls file get installed correctly in Houdini, and that Houdini reports them as installed. """ # The alembic app is added and it should have installed two otl files, # check that Houdini recognizes this. alembic_app = self.engine.apps["tk-houdini-alembicnode"] otl_path = self.engine._safe_path_join(alembic_app.disk_location, "otls") # The alembic node should have version folders, so remove root folder from the list, # and check that we have one path left which will be the version folder. otl_paths = self.engine._get_otl_paths(otl_path) otl_paths.remove(otl_path) self.assertTrue(len(otl_paths) == 1) # Now check both otls were installed in Houdini. sanitized_loaded_files = [ ShotgunPath.from_current_os_path(path) for path in hou.hda.loadedFiles() ] self.assertTrue( ShotgunPath.from_current_os_path( os.path.join(otl_paths[0], "sgtk_alembic.otl")) in sanitized_loaded_files) self.assertTrue( ShotgunPath.from_current_os_path( os.path.join(otl_paths[0], "sgtk_alembic_sop.otl")) in sanitized_loaded_files)
def _browse_path(self, platform): """Browse and set the path for the supplied platform. :param platform: A string indicating the platform to associate with the browsed path. Should match the strings returned by ``sys.platform``. """ # create the dialog folder_path = QtGui.QFileDialog.getExistingDirectory( parent=self, caption="Choose Storage Root Folder", options=QtGui.QFileDialog.DontResolveSymlinks | QtGui.QFileDialog.DontUseNativeDialog | QtGui.QFileDialog.ShowDirsOnly ) if not folder_path: return # create the SG path object. assigning the path to the corresponding # OS property below will sanitize sg_path = ShotgunPath() if platform.startswith("linux"): sg_path.linux = folder_path self.ui.linux_path_edit.setText(sg_path.linux) elif platform == "darwin": sg_path.macosx = folder_path self.ui.mac_path_edit.setText(sg_path.macosx) elif platform == "win32": sg_path.windows = folder_path self.ui.windows_path_edit.setText(sg_path.windows)
def __check_paths(self, houdini_version, expected_folders): """ Checks that the expected folders are gathered for the correct Houdini version. :param houdini_version: The version of Houdini as a tuple of three ints. :param expected_folders: The list of paths that we expect want to compare against the engine generated ones. :return: """ # Change what the engine thinks the Houdini version is. self.engine._houdini_version = houdini_version # Ask the engine for the otl paths. paths_from_engine = self.engine._get_otl_paths(self.app_otl_folder) # We would always expect to get the root otl folder returned. expected_folders.insert(0, self.app_otl_folder) # Handle forward and backwards slashes so that the comparison doesn't care. sanitized_paths_from_engine = [ ShotgunPath.from_current_os_path(path) for path in paths_from_engine ] sanitized_expected_paths = [ ShotgunPath.from_current_os_path(path) for path in expected_folders ] self.assertEqual( sanitized_paths_from_engine, sanitized_expected_paths, "Houdini version number was: v%s.%s.%s" % houdini_version, )
def test_existing_files_not_overwritten(self): """ Ensures that if there were already interpreter files present in the config that they won't be overwritten. """ descriptor = self._write_mock_config() interpreter_yml_path = ShotgunPath.get_file_name_from_template( os.path.join(descriptor.get_path(), "core", "interpreter_%s.cfg") ) # Do not write sys.executable in this file, otherwise we won't know if we're reading our value # or the default value. This means however that we'll have to present the file exists when # os.path.exists is called. os.makedirs(os.path.dirname(interpreter_yml_path)) path = os.path.join("a", "b", "c") with open(interpreter_yml_path, "w") as fh: fh.write(path) # We're going to pretend the interpreter location exists with patch("os.path.exists", return_value=True): # Check that our descriptors sees the value we just wrote to disk self.assertEqual( descriptor.python_interpreter, path ) # Copy the descriptor to its location. descriptor.copy(os.path.join(self._cw.path.current_os, "config")) # have the interpreter files be written out by the writer. The interpreter file we just # wrote should have been left alone. self.assertEqual( self._write_interpreter_file().current_os, path )
def test_transactions(self): """ Ensures the transaction flags are properly handled for a config. """ new_config_root = os.path.join(self.tank_temp, self.short_test_name) writer = ConfigurationWriter( ShotgunPath.from_current_os_path(new_config_root), self.mockgun ) # Test standard transaction flow. # Non pending -> Pending -> Non pending self.assertEqual(False, writer.is_transaction_pending()) writer.start_transaction() self.assertEqual(True, writer.is_transaction_pending()) writer.end_transaction() self.assertEqual(False, writer.is_transaction_pending()) # Remove the transaction folder writer._delete_state_file(writer._TRANSACTION_START_FILE) writer._delete_state_file(writer._TRANSACTION_END_FILE) # Even if the marker is missing, the API should report no pending transactions since the # transaction folder doesn't even exist, which will happen for configurations written # with a previous version of core. self.assertEqual(False, writer.is_transaction_pending()) # We've deleted both the transaction files and now we're writing the end transaction file. # If we're in that state, we'll assume something is broken and say its pending since the # config was tinkered with. writer.end_transaction() self.assertEqual(True, writer.is_transaction_pending())
def test_installed_configuration_not_on_disk(self): """ Ensure that the resolver detects when an installed configuration has not been set for the current platform. """ # Create a pipeline configuration. pc_id = self._create_pc( "Primary", self._project, "sg_path", plugin_ids="foo.*", )["id"] # Remove the current platform's path. self.mockgun.update( "PipelineConfiguration", pc_id, { ShotgunPath.get_shotgun_storage_key(): None } ) with self.assertRaisesRegex( sgtk.bootstrap.TankBootstrapError, "The Shotgun pipeline configuration with id %s has no source location specified for " "your operating system." % pc_id ): self.resolver.resolve_shotgun_configuration( pipeline_config_identifier=pc_id, fallback_config_descriptor=self.config_1, sg_connection=self.mockgun, current_login="******" )
def test_load_snapshot(self): """ Tests loading a snapshot, there is no API method for this, so we are calling internal app functions. """ # Create a file for the test to load. file_path = self._create_file("banana") # Reset the scene so it won't prompt the test to save. self._reset_scene() handler = self.app.tk_multi_snapshot.Snapshot(self.app) handler._do_scene_operation("open", file_path) # Now check that the file Houdini has open is the same as the one we originally saved. self.assertEqual( ShotgunPath.from_current_os_path(file_path), ShotgunPath.from_current_os_path(hou.hipFile.name()), )
def setUp(self): # Makes sure every unit test run in its own sandbox. super(TestInterpreterFilesWriter, self).setUp() self._root = os.path.join(self.tank_temp, self.short_test_name) os.makedirs(self._root) self._cw = ConfigurationWriter( ShotgunPath.from_current_os_path(self._root), self.mockgun)
def test_installed_configuration_not_on_disk(self): """ Ensure that the resolver detects when an installed configuration has not been set for the current platform. """ # Create a pipeline configuration. pc_id = self._create_pc( "Primary", self._project, "sg_path", plugin_ids="foo.*", )["id"] # Remove the current platform's path. self.mockgun.update("PipelineConfiguration", pc_id, {ShotgunPath.get_shotgun_storage_key(): None}) with self.assertRaisesRegexp( sgtk.bootstrap.TankBootstrapError, "The Shotgun pipeline configuration with id %s has no source location specified for " "your operating system." % pc_id): self.resolver.resolve_shotgun_configuration( pipeline_config_identifier=pc_id, fallback_config_descriptor=self.config_1, sg_connection=self.mockgun, current_login="******")
def test_configuration_not_found_on_disk(self): """ Ensure that the resolver detects when an installed configuration is not available for the current platform. """ this_path_does_not_exists = "/this/does/not/exists/on/disk" pc_id = self._create_pc( "Primary", None, this_path_does_not_exists )["id"] expected_descriptor_dict = ShotgunPath( this_path_does_not_exists, this_path_does_not_exists, this_path_does_not_exists ).as_shotgun_dict() expected_descriptor_dict["type"] = "path" with self.assertRaisesRegexp( sgtk.bootstrap.TankBootstrapError, "Installed pipeline configuration '.*' does not exist on disk!" ): self.resolver.resolve_shotgun_configuration( pc_id, [], self.mockgun, "john.smith" )
def test_existing_files_not_overwritten(self): """ Ensures that if there were already interpreter files present in the config that they won't be overwritten. """ descriptor = self._write_mock_config() interpreter_yml_path = ShotgunPath.get_file_name_from_template( os.path.join(descriptor.get_path(), "core", "interpreter_%s.cfg")) # Do not write sys.executable in this file, otherwise we won't know if we're reading our value # or the default value. This means however that we'll have to present the file exists when # os.path.exists is called. os.makedirs(os.path.dirname(interpreter_yml_path)) path = os.path.join("a", "b", "c") with open(interpreter_yml_path, "w") as fh: fh.write(path) # We're going to pretend the interpreter location exists with patch("os.path.exists", return_value=True): # Check that our descriptors sees the value we just wrote to disk self.assertEqual(descriptor.python_interpreter, path) # Copy the descriptor to its location. descriptor.copy(os.path.join(self._cw.path.current_os, "config")) # have the interpreter files be written out by the writer. The interpreter file we just # wrote should have been left alone. self.assertEqual(self._write_interpreter_file().current_os, path)
def _create_test_data(self, create_project): """ Creates test data, including - __pipeline_configuration, a shotgun entity dict. - optional __project entity dict, linked from the pipeline configuration - __descriptor, a sgtk.descriptor.Descriptor refering to a config on disk. - __cw, a ConfigurationWriter """ if create_project: self.__project = self.mockgun.create("Project", { "code": "TestWritePipelineConfigFile", "tank_name": "pc_tank_name" }) else: self.__project = None self.__pipeline_configuration = self.mockgun.create( "PipelineConfiguration", { "code": "PC_TestWritePipelineConfigFile", "project": self.__project }) self.__descriptor = sgtk.descriptor.create_descriptor( self.mockgun, sgtk.descriptor.Descriptor.CONFIG, dict(type="dev", path="/a/b/c")) config_root = os.path.join(self.tank_temp, self.id()) self.__cw = ConfigurationWriter( ShotgunPath.from_current_os_path(config_root), self.mockgun) os.makedirs(os.path.join(config_root, "config", "core"))
def _write_interpreter_file(self, executable=sys.executable, prefix=sys.prefix): """ Writes the interpreter file to disk based on an executable and prefix. :returns: Path that was written in each interpreter file. :rtype: sgtk.util.ShotgunPath """ core_folder = os.path.join(self._root, "config", "core") if not os.path.exists(core_folder): os.makedirs(core_folder) os.makedirs( os.path.join(self._root, "install", "core", "setup", "root_binaries")) self._cw.create_tank_command(executable, prefix) interpreters = [] for platform in ["Windows", "Linux", "Darwin"]: file_name = os.path.join(self._root, "config", "core", "interpreter_%s.cfg" % platform) with open(file_name, "r") as w: interpreters.append(w.read()) return ShotgunPath(*interpreters)
def test_transactions(self): """ Ensures the transaction flags are properly handled for a config. """ new_config_root = os.path.join(self.tank_temp, self.short_test_name) writer = ConfigurationWriter( ShotgunPath.from_current_os_path(new_config_root), self.mockgun) # Test standard transaction flow. # Non pending -> Pending -> Non pending self.assertEqual(False, writer.is_transaction_pending()) writer.start_transaction() self.assertEqual(True, writer.is_transaction_pending()) writer.end_transaction() self.assertEqual(False, writer.is_transaction_pending()) # Remove the transaction folder writer._delete_state_file(writer._TRANSACTION_START_FILE) writer._delete_state_file(writer._TRANSACTION_END_FILE) # Even if the marker is missing, the API should report no pending transactions since the # transaction folder doesn't even exist, which will happen for configurations written # with a previous version of core. self.assertEqual(False, writer.is_transaction_pending()) # We've deleted both the transaction files and now we're writing the end transaction file. # If we're in that state, we'll assume something is broken and say its pending since the # config was tinkered with. writer.end_transaction() self.assertEqual(True, writer.is_transaction_pending())
def _get_default_intepreters(self): """ Gets the default interpreter values for the Shotgun Desktop. """ return ShotgunPath( r"C:\Program Files\Shotgun\Python\python.exe", "/opt/Shotgun/Python/bin/python", "/Applications/Shotgun.app/Contents/Resources/Python/bin/python")
def test_get_current_path(self): """ Tests the scene operation hooks current_path operation. """ # Create a temporary scene file, so we can test that we can get the current path to it. created_file = self._create_file("temp") # Make sure the scene file we created matches what Houdini believes to be the scene file. self.assertEqual(hou.hipFile.name(), created_file) result = self.scene_operation.get_current_path( self.app, self.scene_operation.NEW_FILE_ACTION, self.engine.context ) self.assertEqual( ShotgunPath.from_current_os_path(hou.hipFile.name()), ShotgunPath.from_current_os_path(result), )
def setUp(self): # Makes sure every unit test run in its own sandbox. super(TestInterpreterFilesWriter, self).setUp() self._root = os.path.join(self.tank_temp, self.short_test_name) os.makedirs(self._root) self._cw = ConfigurationWriter( ShotgunPath.from_current_os_path(self._root), self.mockgun )
def _create_test_data(self, create_project): """ Creates test data, including - __site_configuration, a shotgun entity dict. - optional __project entity dict, linked from the pipeline configuration - __descriptor, a sgtk.descriptor.Descriptor refering to a config on disk. - __cw, a ConfigurationWriter """ if create_project: self.__project = self.mockgun.create( "Project", { "code": "TestWritePipelineConfigFile", "tank_name": "pc_tank_name" } ) else: self.__project = None self.__site_configuration = self.mockgun.create( "PipelineConfiguration", { "code": "PC_TestWritePipelineConfigFile", "project": None } ) self.__project_configuration = self.mockgun.create( "PipelineConfiguration", { "code": "PC_TestWritePipelineConfigFile", "project": self.__project } ) self.__descriptor = sgtk.descriptor.create_descriptor( self.mockgun, sgtk.descriptor.Descriptor.CONFIG, dict(type="dev", path="/a/b/c") ) config_root = os.path.join(self.tank_temp, self.short_test_name) self.__cw = ConfigurationWriter( ShotgunPath.from_current_os_path(config_root), self.mockgun ) os.makedirs( os.path.join( config_root, "config", "core" ) )
def _open_browser(self): """ Method called to open the folder browser. """ self.ui.vhd_location_le.clear() dir = QtGui.QFileDialog.getExistingDirectory(parent=self) if dir: norm_path = ShotgunPath.normalize(dir) if not norm_path.endswith('\\'): norm_path = '{0}\\'.format(norm_path) self.ui.vhd_location_le.insert(norm_path)
def test_reset(self): """ Tests the scene operation hooks reset operation. """ # Create a temporary scene file, so we can test the reset works. created_file = self._create_file("temp") # Make sure the scene file we created matches what Houdini believes to be the scene file. self.assertEqual( ShotgunPath.from_current_os_path(hou.hipFile.name()), ShotgunPath.from_current_os_path(created_file), ) result = self.scene_operation.reset_current_scene( self.app, self.scene_operation.NEW_FILE_ACTION, self.engine.context ) self.assertTrue(result) # When we reset the file name should be untitled.hip self.assertEqual( ShotgunPath.from_current_os_path(hou.hipFile.name()), ShotgunPath.from_current_os_path("untitled.hip"), )
def setUp(self): super(TestTankFromPathDuplicatePcPaths, self).setUp() # define an additional pipeline config with overlapping paths self.overlapping_pc = { "type": "PipelineConfiguration", "code": "Primary", "id": 123456, "project": self.project, ShotgunPath.get_shotgun_storage_key(): self.project_root, } self.add_to_sg_mock_db(self.overlapping_pc)
def setUp(self): super(TestTankFromPathDuplicatePcPaths, self).setUp() # define an additional pipeline config with overlapping paths self.overlapping_pc = { "type": "PipelineConfiguration", "code": "Primary", "id": 123456, "project": self.project, ShotgunPath.get_shotgun_storage_key(): self.project_root, } self.add_to_sg_mock_db(self.overlapping_pc)
def test_open_file(self): """ Tests the scene operation hooks open operation. """ created_file = self._create_file("dog") # Reset the scene so it is empty in preparation for opening the file we just saved. self._reset_scene() self.scene_operation.open_file( self.app, self.scene_operation.NEW_FILE_ACTION, self.engine.context, created_file, 1, False, ) self.assertEqual( ShotgunPath.from_current_os_path(created_file), ShotgunPath.from_current_os_path(hou.hipFile.name()), )
def test_save_file(self): """ Tests the scene operation hooks save operation. """ save_path = self._get_new_file_path("work_path", "cat") # test saving a new file. self.scene_operation.save_file( self.app, self.scene_operation.NEW_FILE_ACTION, self.engine.context, path=save_path, ) self.assertEqual( ShotgunPath.from_current_os_path(save_path), ShotgunPath.from_current_os_path(hou.hipFile.name()), ) # Now test saving over the same file. self.scene_operation.save_file( self.app, self.scene_operation.NEW_FILE_ACTION, self.engine.context )
def _create_configuration_writer(self): """ Creates a configuration writer that will write to a unique folder for this test. """ new_config_root = os.path.join(self.tank_temp, "new_configuration", self.short_test_name) shotgun_yml_root = os.path.join(new_config_root, "config", "core") # Ensures the location for the shotgun.yml exists. os.makedirs(shotgun_yml_root) writer = ConfigurationWriter( ShotgunPath.from_current_os_path(new_config_root), self.mockgun) writer.ensure_project_scaffold() return writer
def test_character_escaping(self): """ Ensure that the ' characte is properly escaped when writing out install_location.yml """ new_config_root = os.path.join(self.tank_temp, self.short_test_name, "O'Connell") writer = ConfigurationWriter( ShotgunPath.from_current_os_path(new_config_root), self.mockgun) install_location_path = os.path.join(new_config_root, "config", "core", "install_location.yml") os.makedirs(os.path.dirname(install_location_path)) writer.write_install_location_file() with open(install_location_path, "rt") as f: paths = yaml.safe_load(f) path = ShotgunPath(paths["Windows"], paths["Linux"], paths["Darwin"]) assert path.current_os == new_config_root
def test_execute(self): """ Ensure we can set up the project. """ self._wizard.set_project_disk_name(self.short_test_name) path = ShotgunPath.from_current_os_path( os.path.join(self.tank_temp, self.short_test_name, "pipeline") ) self._wizard.set_configuration_location(path.linux, path.windows, path.macosx) # Upload method not implemented on Mockgun yet, so skip that bit. with patch("tank_vendor.shotgun_api3.lib.mockgun.mockgun.Shotgun.upload"): with patch("tank.pipelineconfig_utils.get_core_api_version") as api_mock: api_mock.return_value = "HEAD" self._wizard.execute()
def _create_configuration_writer(self): """ Creates a configuration writer that will write to a unique folder for this test. """ new_config_root = os.path.join(self.tank_temp, "new_configuration", self.short_test_name) shotgun_yml_root = os.path.join(new_config_root, "config", "core") # Ensures the location for the shotgun.yml exists. os.makedirs(shotgun_yml_root) writer = ConfigurationWriter( ShotgunPath.from_current_os_path(new_config_root), self.mockgun ) writer.ensure_project_scaffold() return writer
def test_get_core_settings(self): """ Ensure we can find the core settings. Given this is a unit test and not running off a real core, there's nothing more we can do at the moment. """ # Core is installed as # <studio-install>/install/core/python # This file is under the equivalent of # <studio-install>/install/core/tests/commands_tests/test_project_wizard.py # So we have to pop 4 folders to get back the equivalent location. install_location = os.path.normpath( os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") ) self.assertEqual( self._wizard.get_core_settings(), { "core_path": ShotgunPath.from_current_os_path( install_location ).as_system_dict(), "localize": True, "pipeline_config": None, "using_runtime": True, }, )
def validatePage(self): """The 'next' button was pushed. See if the mappings are valid.""" logger.debug("Validating the storage mappings page...") # the wizard instance and its UI wiz = self.wizard() ui = wiz.ui # clear any errors ui.storage_errors.setText("") # get the path key for the current os current_os_key = ShotgunPath.get_shotgun_storage_key() logger.debug("Current OS storage path key: %s" % (current_os_key, )) # temp lists of widgets that need attention invalid_widgets = [] not_on_disk_widgets = [] # keep track of the first invalid widget so we can ensure it is visible # to the user in the list. first_invalid_widget = None logger.debug("Checking all map widgets...") # see if each of the mappings is valid for map_widget in self._map_widgets: logger.debug("Checking mapping for root: %s" % (map_widget.root_name, )) if not map_widget.mapping_is_valid(): # something is wrong with this widget's mapping invalid_widgets.append(map_widget) if first_invalid_widget is None: first_invalid_widget = map_widget storage = map_widget.local_storage or {} current_os_path = storage.get(current_os_key) if current_os_path and not os.path.exists(current_os_path): # the current os path for this widget doesn't exist on disk not_on_disk_widgets.append(map_widget) if invalid_widgets: # tell the user which roots don't have valid mappings root_names = [w.root_name for w in invalid_widgets] logger.debug("Invalid mappings for roots: %s" % (root_names)) ui.storage_errors.setText( "The mappings for these roots are invalid: <b>%s</b>" % (", ".join(root_names), )) if first_invalid_widget: ui.storage_map_area.ensureWidgetVisible(first_invalid_widget) return False if not_on_disk_widgets: # try to create the folders for current OS if they don't exist failed_to_create = [] for widget in not_on_disk_widgets: storage = widget.local_storage folder = storage[current_os_key] logger.debug("Ensuring folder on disk for storage '%s': %s" % (storage["code"], folder)) # try to create the missing path for the current OS. this will # help ensure the storage specified in SG is valid and the # project data can be written to this root. try: ensure_folder_exists(folder) except Exception: logger.error("Failed to create folder: %s" % (folder, )) logger.error(traceback.format_exc()) failed_to_create.append(storage["code"]) if failed_to_create: # some folders weren't created. let the user know. ui.storage_errors.setText( "Unable to create folders on disk for these storages: %s." "Please check to make sure you have permission to create " "these folders. See the tk-desktop log for more info." % (", ".join(failed_to_create), )) # ---- now we've mapped the roots, and they're all valid, we need to # update the root information on the core wizard for map_widget in self._map_widgets: root_name = map_widget.root_name root_info = map_widget.root_info storage_data = map_widget.local_storage # populate the data defined prior to mapping updated_storage_data = root_info # update the mapped shotgun data updated_storage_data["shotgun_storage_id"] = storage_data["id"] updated_storage_data["linux_path"] = str( storage_data["linux_path"]) updated_storage_data["mac_path"] = str(storage_data["mac_path"]) updated_storage_data["windows_path"] = str( storage_data["windows_path"]) # now update the core wizard's root info wiz.core_wizard.update_storage_root(self._uri, root_name, updated_storage_data) # store the fact that we've mapped this root name with this # storage name. we can use this information to make better # guesses next time this user is mapping storages. self._historical_mappings[root_name] = storage_data["code"] self._settings.store(self.HISTORICAL_MAPPING_KEY, self._historical_mappings) logger.debug("Storage mappings are valid.") # if we made it here, then we should be valid. try: wiz.core_wizard.set_config_uri(self._uri) except Exception as e: error = ("Unknown error when setting the configuration uri:\n%s" % str(e)) logger.error(error) logger.error(traceback.print_exc()) ui.storage_errors.setText(error) return False return True
CREATE_DEFAULT_LOCATION = ShotgunPath.from_shotgun_dict({ "windows_path": os.path.abspath( os.path.join( os.sep, # The name of the folder can change based on the locale. # https://www.samlogic.net/articles/program-files-folder-different-languages.htm # The safe way to retrieve the path to the Program Files folder is to read the env var # Warning: Running this code on non-window trigger a KeyError # This is why we use get with a dummy default value. os.environ.get("ProgramFiles", "%ProgramFiles%"), "Autodesk", "Shotgun Create", "bin", "ShotgunCreate.exe", )), "mac_path": os.path.abspath( os.path.join( os.sep, "Applications", "Autodesk", "Shotgun Create.app", "Contents", "MacOS", "Shotgun Create", )), "linux_path": os.path.abspath( os.path.join(os.sep, "opt", "Autodesk", "ShotgunCreate", "bin", "ShotgunCreate")), })
def _on_path_changed(self, path, platform): """ Keep track of any path edits as they happen. Keep the user informed if there are any concerns about the entered text. :param path: The path that has changed. :param platform: The platform the modified path is associated with. """ # does the path only contain slashes? only_slashes = path.replace("/", "").replace("\\", "") == "" # does it end in a slash? trailing_slash = path.endswith("/") or path.endswith("\\") # the name of the storage being edited storage_name = str(self.ui.storage_select_combo.currentText()) # a temp SG path object used for sanitization sg_path = ShotgunPath() # store the edited path in the appropriate path lookup. sanitize first # by running it through the ShotgunPath object. since sanitize removes # the trailing slash, add it back in if the user typed it. # if the sanitized path differs, update the edit. if platform.startswith("linux"): if only_slashes: # SG path code doesn't like only slashes in a path self._linux_path_edit[storage_name] = path elif path: sg_path.linux = path # sanitize sanitized_path = sg_path.linux if trailing_slash: # add the trailing slash back in sanitized_path = "%s/" % (sanitized_path,) if sanitized_path != path: # path changed due to sanitation. change it in the UI self.ui.linux_path_edit.setText(sanitized_path) # remember the sanitized path self._linux_path_edit[storage_name] = sanitized_path else: # no path. update the edit lookup to reflect self._linux_path_edit[storage_name] = "" elif platform == "darwin": if only_slashes: # SG path code doesn't like only slashes in a path self._mac_path_edit[storage_name] = path elif path: sg_path.macosx = path # sanitize sanitized_path = sg_path.macosx if trailing_slash: # add the trailing slash back in sanitized_path = "%s/" % (sanitized_path,) if sanitized_path != path: # path changed due to sanitation. change it in the UI self.ui.mac_path_edit.setText(sanitized_path) # remember the sanitized path self._mac_path_edit[storage_name] = sanitized_path else: # no path. update the edit lookup to reflect self._mac_path_edit[storage_name] = "" elif platform == "win32": if only_slashes: # SG path code doesn't like only slashes in a path self._windows_path_edit[storage_name] = path elif path: sg_path.windows = path # sanitize sanitized_path = sg_path.windows if trailing_slash and not sanitized_path.endswith("\\"): # add the trailing slash back in sanitized_path = "%s\\" % (sanitized_path,) if sanitized_path != path: # path changed due to sanitation. change it in the UI self.ui.windows_path_edit.setText(sanitized_path) # remember the sanitized path self._windows_path_edit[storage_name] = sanitized_path else: # no path. update the edit lookup to reflect self._windows_path_edit[storage_name] = "" # run the validation tell the user if there are issues self.mapping_is_valid()
def validatePage(self): """The 'next' button was pushed. See if the mappings are valid.""" logger.debug("Validating the storage mappings page...") # the wizard instance and its UI wiz = self.wizard() ui = wiz.ui # clear any errors ui.storage_errors.setText("") # get the path key for the current os current_os_key = ShotgunPath.get_shotgun_storage_key() logger.debug("Current OS storage path key: %s" % (current_os_key,)) # temp lists of widgets that need attention invalid_widgets = [] not_on_disk_widgets = [] # keep track of the first invalid widget so we can ensure it is visible # to the user in the list. first_invalid_widget = None logger.debug("Checking all map widgets...") # see if each of the mappings is valid for map_widget in self._map_widgets: logger.debug( "Checking mapping for root: %s" % (map_widget.root_name,) ) if not map_widget.mapping_is_valid(): # something is wrong with this widget's mapping invalid_widgets.append(map_widget) if first_invalid_widget is None: first_invalid_widget = map_widget storage = map_widget.local_storage or {} current_os_path = storage.get(current_os_key) if current_os_path and not os.path.exists(current_os_path): # the current os path for this widget doesn't exist on disk not_on_disk_widgets.append(map_widget) if invalid_widgets: # tell the user which roots don't have valid mappings root_names = [w.root_name for w in invalid_widgets] logger.debug("Invalid mappings for roots: %s" % (root_names)) ui.storage_errors.setText( "The mappings for these roots are invalid: <b>%s</b>" % (", ".join(root_names),) ) if first_invalid_widget: ui.storage_map_area.ensureWidgetVisible(first_invalid_widget) return False if not_on_disk_widgets: # try to create the folders for current OS if they don't exist failed_to_create = [] for widget in not_on_disk_widgets: storage = widget.local_storage folder = storage[current_os_key] logger.debug( "Ensuring folder on disk for storage '%s': %s" % (storage["code"], folder) ) # try to create the missing path for the current OS. this will # help ensure the storage specified in SG is valid and the # project data can be written to this root. try: ensure_folder_exists(folder) except Exception: logger.error("Failed to create folder: %s" % (folder,)) logger.error(traceback.format_exc()) failed_to_create.append(storage["code"]) if failed_to_create: # some folders weren't created. let the user know. ui.storage_errors.setText( "Unable to create folders on disk for these storages: %s." "Please check to make sure you have permission to create " "these folders. See the tk-desktop log for more info." % (", ".join(failed_to_create),) ) # ---- now we've mapped the roots, and they're all valid, we need to # update the root information on the core wizard for map_widget in self._map_widgets: root_name = map_widget.root_name root_info = map_widget.root_info storage_data = map_widget.local_storage # populate the data defined prior to mapping updated_storage_data = root_info # update the mapped shotgun data updated_storage_data["shotgun_storage_id"] = storage_data["id"] updated_storage_data["linux_path"] = str(storage_data["linux_path"]) updated_storage_data["mac_path"] = str(storage_data["mac_path"]) updated_storage_data["windows_path"] = str( storage_data["windows_path"]) # now update the core wizard's root info wiz.core_wizard.update_storage_root( self._uri, root_name, updated_storage_data ) # store the fact that we've mapped this root name with this # storage name. we can use this information to make better # guesses next time this user is mapping storages. self._historical_mappings[root_name] = storage_data["code"] self._settings.store( self.HISTORICAL_MAPPING_KEY, self._historical_mappings ) logger.debug("Storage mappings are valid.") # if we made it here, then we should be valid. try: wiz.core_wizard.set_config_uri(self._uri) except Exception as e: error = ( "Unknown error when setting the configuration uri:\n%s" % str(e) ) logger.error(error) logger.error(traceback.print_exc()) ui.storage_errors.setText(error) return False return True
class TestSetupProjectWizard(TankTestBase): """ Makes sure environment code works with the app store mocker. """ def setUp(self): """ Prepare unit test. """ super(TestSetupProjectWizard, self).setUp( parameters={"primary_root_name": "primary"} ) self._wizard = sgtk.get_command("setup_project_factory").execute({}) self._storage_locations = ShotgunPath( "Z:\\projects", "/mnt/projects", "/Volumes/projects" ) self._storage_locations.current_os = self.tank_temp self.mockgun.update( "LocalStorage", self.primary_storage["id"], self._storage_locations.as_shotgun_dict(), ) # Prepare the wizard for business. All these methods are actually passing # information directly to the SetupProjectParams object inside # the wizard, so there's no need to test them per-se. self._wizard.set_project(self.project["id"], force=True) self._wizard.set_use_distributed_mode() self.config_uri = os.path.join(self.fixtures_root, "config") self._wizard.set_config_uri(self.config_uri) def test_validate_config_uri(self): """ Ensure that we can validate the URI. This doesn't actually test much, it is simply there as a proof that there is a bug in the API right now. We should get back to this in the future. """ storage_setup = self._wizard.validate_config_uri(self.config_uri) expected_primary_storage = { "default": True, "defined_in_shotgun": True, "description": "Default location where project data is stored.", "exists_on_disk": True, "shotgun_id": self.primary_storage["id"], # FIXME: This is a bug. The StorageRoots instance, owned by the SetupProjectParams, # is initialized to these values by default. They are then injected into # the result of validate_config_uri. validate_config_uri is expected # however to return paths named after sys.platform and not <os>_path. # We can review this once the Python 3 port is done. "linux_path": "/studio/projects", "mac_path": "/studio/projects", "windows_path": "\\\\network\\projects", } # Inject the storage locations we set on the local storage earlier. expected_primary_storage.update(self._storage_locations.as_system_dict()) self.assertEqual(storage_setup, {"primary": expected_primary_storage}) def test_set_project_disk_name(self): """ Ensure the project folder gets created or not on demand. """ # Make sure the config we have is valid. project_locations = self._storage_locations.join(self.short_test_name) self._wizard.set_project_disk_name(self.short_test_name, False) self.assertFalse(os.path.exists(project_locations.current_os)) self._wizard.set_project_disk_name(self.short_test_name, True) self.assertTrue(os.path.exists(project_locations.current_os)) def test_preview_project_paths(self): """ Ensure all project paths get returned properly. """ self.assertEqual( self._wizard.preview_project_paths(self.short_test_name), { "primary": self._storage_locations.join( self.short_test_name ).as_system_dict() }, ) def test_default_configuration_location_without_suggestions(self): """ Ensure that when no matching pipeline configurations are found that we do not get a suggestion back. """ self._wizard.set_project_disk_name(self.short_test_name) locations = self._wizard.get_default_configuration_location() self.assertEqual(locations, {"win32": None, "darwin": None, "linux2": None}) def test_default_configuration_location_with_existing_pipeline_configuration(self): """ Ensure that when the tank_name and the configuration folder name are the same for the latest configuration found in Shotgun, we'll offer a pre-baked path to the user using the new project name. For e.g., if a project with a tank_name set to "potato" and whose configuration was written to "/vegatables/potato", then a new project with tank name "radish" would get a default location of "/vegatables/radish". """ self._wizard.set_project_disk_name(self.short_test_name) # Create a project with a tank name matching the name of the folder for the pipeline configuration other_project = self.mockgun.create( "Project", {"name": "Other Project", "tank_name": "other_project"} ) self.mockgun.create( "PipelineConfiguration", { "code": "primary", "created_at": datetime.datetime.now(), "project": other_project, "mac_path": "/Volumes/configs/other_project", "linux_path": "/mnt/configs/other_project", "windows_path": "Z:\\configs\\other_project", }, ) locations = self._wizard.get_default_configuration_location() self.assertEqual( locations, { "darwin": "/Volumes/configs/{0}".format(self.short_test_name), "linux2": "/mnt/configs/{0}".format(self.short_test_name), "win32": "Z:\\configs\\{0}".format(self.short_test_name), }, ) def test_get_core_settings(self): """ Ensure we can find the core settings. Given this is a unit test and not running off a real core, there's nothing more we can do at the moment. """ # Core is installed as # <studio-install>/install/core/python # This file is under the equivalent of # <studio-install>/install/core/tests/commands_tests/test_project_wizard.py # So we have to pop 4 folders to get back the equivalent location. install_location = os.path.normpath( os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") ) self.assertEqual( self._wizard.get_core_settings(), { "core_path": ShotgunPath.from_current_os_path( install_location ).as_system_dict(), "localize": True, "pipeline_config": None, "using_runtime": True, }, ) def test_execute(self): """ Ensure we can set up the project. """ self._wizard.set_project_disk_name(self.short_test_name) path = ShotgunPath.from_current_os_path( os.path.join(self.tank_temp, self.short_test_name, "pipeline") ) self._wizard.set_configuration_location(path.linux, path.windows, path.macosx) # Upload method not implemented on Mockgun yet, so skip that bit. with patch("tank_vendor.shotgun_api3.lib.mockgun.mockgun.Shotgun.upload"): with patch("tank.pipelineconfig_utils.get_core_api_version") as api_mock: api_mock.return_value = "HEAD" self._wizard.execute()