def test_next_class(self): """ It should be possible to invoke operations on a TempDir and get Path classes. """ d = TempDir() sub = d / 'subdir' assert isinstance(sub, path.Path) d.rmdir()
class TestCaseKara(TestCase): """Test class that creates a working kara folder """ def setUp(self): # create kara folder self.kara_folder = TempDir() # create subtitle with path("tests.resources", "song1.ass") as file: self.subtitle1_path = Path(file).copy(self.kara_folder) with path("tests.resources", "song2.ass") as file: self.subtitle2_path = Path(file).copy(self.kara_folder) # create song with path("tests.resources", "song1.mkv") as file: self.song1_path = Path(file).copy(self.kara_folder) with path("tests.resources", "song2.mkv") as file: self.song2_path = Path(file).copy(self.kara_folder) with path("tests.resources", "song3.avi") as file: self.song3_path = Path(file).copy(self.kara_folder) # create audio with path("tests.resources", "song2.mp3") as file: self.audio2_path = Path(file).copy(self.kara_folder) # create playlist entry self.playlist_entry1 = { "id": 42, "song": {"title": "Song 1", "file_path": self.song1_path}, "owner": "me", "use_instrumental": False, } self.playlist_entry2 = { "id": 43, "song": {"title": "Song 2", "file_path": self.song2_path}, "owner": "me", "use_instrumental": False, } self.playlist_entry3 = { "id": 44, "song": {"title": "Song 3", "file_path": self.song3_path}, "owner": "me", "use_instrumental": False, } def tearDown(self): self.kara_folder.rmtree(ignore_errors=True)
def setUp(self): self.dirname = path(pkg_resources.resource_filename(__name__, "")) self.build_dir = TempDir() os.environ["CHARM_HIDE_METRICS"] = 'true' os.environ["CHARM_LAYERS_DIR"] = self.dirname / "layers" os.environ["CHARM_INTERFACES_DIR"] = self.dirname / "interfaces" os.environ["CHARM_CACHE_DIR"] = self.build_dir / "_cache" os.environ.pop("JUJU_REPOSITORY", None) os.environ.pop("LAYER_PATH", None) os.environ.pop("INTERFACE_PATH", None) self.p_post = mock.patch('requests.post') self.p_post.start()
def test_context_manager(self): """ One should be able to use a TempDir object as a context, which will clean up the contents after. """ d = TempDir() res = d.__enter__() assert res == path.Path(d) (d / 'somefile.txt').touch() assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(None, None, None) assert not d.exists()
def test_samefile(self, tmpdir): f1 = (TempDir() / '1.txt').touch() f1.write_text('foo') f2 = (TempDir() / '2.txt').touch() f1.write_text('foo') f3 = (TempDir() / '3.txt').touch() f1.write_text('bar') f4 = TempDir() / '4.txt' f1.copyfile(f4) assert os.path.samefile(f1, f2) == f1.samefile(f2) assert os.path.samefile(f1, f3) == f1.samefile(f3) assert os.path.samefile(f1, f4) == f1.samefile(f4) assert os.path.samefile(f1, f1) == f1.samefile(f1)
def save_profile_picture(backend, user, response, *args, **kwargs): """ This method is called in the social pipeline when a user logs in using its Google account. It creates a profile, if needed, and updates the profile picture. """ profile = Profile.objects.filter(user=user).first() if profile is None: profile = Profile(user=user) if "locale" in response: profile.locale = response["locale"] if "picture" in response: with TempDir() as d: _name, ext = splitext(response["picture"]) r = requests.get(response["picture"], stream=True) if r.ok: temp_file = d / f"{user.pk}{ext or '.jpg'}" with open(temp_file, "wb") as fd: for chunk in r.iter_content(chunk_size=128): fd.write(chunk) with temp_file.open("rb") as fd: profile.picture.save(f"{user.pk}{ext or '.jpg'}", File(fd), save=False) profile.save()
def extract(cls, input_file_path): """Extract lyrics form a file Try to extract the first subtitle of the given input file into the output file given. Args: input_file_path (str): path to the input file. """ if not cls.is_available(): raise FFmpegNotInstalledError("FFmpeg not installed") with TempDir() as directory_path: output_file_path = directory_path / "output.ass" process = subprocess.run( [ "ffmpeg", "-i", input_file_path, "-map", "0:s:0", output_file_path ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # if call failed, return empty string if process.returncode: return cls() # otherwise extract content return cls(output_file_path.text())
def test_load_templates_custom(self): """Test to load custom templates using an existing directory """ with TempDir() as temp: # prepare directory with path("dakara_player.resources.templates", "idle.ass") as file: Path(file).copy(temp) with path("dakara_player.resources.templates", "transition.ass") as file: Path(file).copy(temp) # create object text_generator = TextGenerator( package="dakara_player.resources.templates", directory=temp, filenames={ "idle": "idle.ass", "transition": "transition.ass" }, ) # call the method text_generator.load_templates() # assert there are templates defined loader_custom, loader_default = text_generator.environment.loader.loaders self.assertIn("idle.ass", loader_custom.list_templates()) self.assertIn("transition.ass", loader_custom.list_templates())
def test_load_templates_default(self): """Test to load default templates using an existing directory Integration test. """ with TempDir() as temp: # create object text_generator = TextGenerator( package="dakara_player.resources.templates", directory=temp, filenames={ "idle": "idle.ass", "transition": "transition.ass" }, ) # call the method text_generator.load_templates() # assert there are templates defined loader_custom, loader_default = text_generator.environment.loader.loaders self.assertNotIn("idle.ass", loader_custom.list_templates()) self.assertNotIn("transition.ass", loader_custom.list_templates()) self.assertIn("idle.ass", loader_default.list_templates()) self.assertIn("transition.ass", loader_default.list_templates())
def get_instance(self, config=None, check_error=True): """Get an instance of MediaPlayerMpv for the available version This method is a context manager that automatically stops the player on exit. Args: config (dict): Configuration passed to the constructor. check_error (bool): If true, check if the player stop event is not set and the error queue is empty at the end. Yields: tuple: Containing the following elements: MediaPlayerMpv: Instance; path.Path: Path of the temporary directory; unittest.case._LoggingWatcher: Captured output. """ if not config: config = { "kara_folder": self.kara_folder, "fullscreen": self.fullscreen, "mpv": { "vo": "null", "ao": "null" }, } with TempDir() as temp: try: with ExitStack() as stack: mpv_player = stack.enter_context( MediaPlayerMpv.from_version(Event(), Queue(), config, temp, warn_long_exit=False)) output = stack.enter_context( self.assertLogs("dakara_player.media_player.mpv", "DEBUG")) mpv_player.load() yield mpv_player, temp, output if check_error: # display errors in queue if any if not mpv_player.errors.empty(): _, error, traceback = mpv_player.errors.get(5) error.with_traceback(traceback) raise error # assert no errors to fail test if any self.assertFalse(mpv_player.stop.is_set()) except OSError: # silence closing errors of mpv pass # sleep to allow slow systems to correctly clean up sleep(self.DELAY)
def setUp(self): # create kara folder self.kara_folder = TempDir() # create subtitle with path("tests.resources", "song1.ass") as file: self.subtitle1_path = Path(file).copy(self.kara_folder) with path("tests.resources", "song2.ass") as file: self.subtitle2_path = Path(file).copy(self.kara_folder) # create song with path("tests.resources", "song1.mkv") as file: self.song1_path = Path(file).copy(self.kara_folder) with path("tests.resources", "song2.mkv") as file: self.song2_path = Path(file).copy(self.kara_folder) with path("tests.resources", "song3.avi") as file: self.song3_path = Path(file).copy(self.kara_folder) # create audio with path("tests.resources", "song2.mp3") as file: self.audio2_path = Path(file).copy(self.kara_folder) # create playlist entry self.playlist_entry1 = { "id": 42, "song": {"title": "Song 1", "file_path": self.song1_path}, "owner": "me", "use_instrumental": False, } self.playlist_entry2 = { "id": 43, "song": {"title": "Song 2", "file_path": self.song2_path}, "owner": "me", "use_instrumental": False, } self.playlist_entry3 = { "id": 44, "song": {"title": "Song 3", "file_path": self.song3_path}, "owner": "me", "use_instrumental": False, }
def test_chunks(self, tmpdir): p = (TempDir() / 'test.txt').touch() txt = "0123456789" size = 5 p.write_text(txt) for i, chunk in enumerate(p.chunks(size)): assert chunk == txt[i * size:i * size + size] assert i == len(txt) / size - 1
def setUp(self): self.dirname = path(pkg_resources.resource_filename(__name__, "")) self.build_dir = TempDir() os.environ["CHARM_HIDE_METRICS"] = 'true' os.environ["CHARM_LAYERS_DIR"] = self.dirname / "layers" os.environ["CHARM_INTERFACES_DIR"] = self.dirname / "interfaces" os.environ["CHARM_CACHE_DIR"] = self.build_dir / "_cache" os.environ.pop("JUJU_REPOSITORY", None) os.environ.pop("LAYER_PATH", None) os.environ.pop("INTERFACE_PATH", None) ifd = build.fetchers.LayerFetcher.LAYER_INDEX self.p_post = mock.patch('requests.post') self.p_post.start() # preserve the layer index between tests self.p_layer_index = mock.patch( 'charmtools.build.fetchers.' 'LayerFetcher.LAYER_INDEX', ifd) self.p_layer_index.start()
def test_context_manager_using_with(self): """ The context manager will allow using the with keyword and provide a temporary directory that will be deleted after that. """ with TempDir() as d: assert d.isdir() assert not d.isdir()
def ensure_eplus_root( url: str, eplus_folder: Path = user_data_dir(appname="energy_plus_wrapper"), installer_cache: Path = None, ) -> str: """Check if the energy plus root is available in the provided eplus_folder, download it from the url, extract and install it if it's not the case. In any cases, return the EnergyPlus folder as needed by the EPlusRunner. This routine is only available for Linux (for now) ! Arguments: url {str} -- the EnergyPlus installer URL. Look at `https://energyplus.net/downloads` Keyword Arguments: eplus_folder {Path} -- where EnergyPlus should be installed, as `{eplus_folder}/{eplus_version}/`. (default: user_data_dir(appname="energy_plus_wrapper")) installer_cache {Path} -- where to download the installation script. If None, a temporary folder will be created. (default: {None}) Returns: [str] -- The EnergyPlus root. """ if platform.system() != "Linux": raise ValueError( f"Your system ({platform.system()}) is not supported yet." " You have to install EnergyPlus by yourself.") eplus_folder = Path(eplus_folder) eplus_folder.mkdir_p() with fasteners.InterProcessLock(eplus_folder / ".lock"): def url_to_installed(url, eplus_folder, script_path): if not script_path.exists(): _download_eplus_version(url, script_path) _extract_and_install(script_path, eplus_folder) finfo = _extract_filename_info(url) filename = finfo["filename"] version = finfo["version"] expected_eplus_folder = eplus_folder / f"EnergyPlus-{version.replace('.', '-')}" if expected_eplus_folder.exists() and expected_eplus_folder.files(): return expected_eplus_folder.abspath() expected_eplus_folder.rmtree_p() if installer_cache is None: with TempDir() as d: url_to_installed(url, eplus_folder, d / filename) else: installer_cache = Path(installer_cache) installer_cache.mkdir_p() url_to_installed(url, eplus_folder, installer_cache / filename) return expected_eplus_folder.abspath()
def test_parse_invalid_error(self): """Test to extract metadata from a file that cannot be parsed """ with TempDir() as temp: file = temp / "file" file.write_bytes(b"nonsense") # call the method with self.assertRaisesRegex(MediaParseError, "Error when processing media file"): FFProbeMetadataParser.parse(file)
def test_constructor(self): """ One should be able to readily construct a temporary directory """ d = TempDir() assert isinstance(d, path.Path) assert d.exists() assert d.isdir() d.rmdir() assert not d.exists()
def test_context_manager_exception(self): """ The context manager will not clean up if an exception occurs. """ d = TempDir() d.__enter__() (d / 'somefile.txt').touch() assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(TypeError, TypeError('foo'), None) assert d.exists()
def get_instance(self, config=None, check_error=True): """Get an instance of MediaPlayerVlc This method is a context manager that automatically stops the player on exit. Args: config (dict): Configuration passed to the constructor. check_error (bool): If true, check if the player stop event is not set and the error queue is empty at the end. Yields: tuple: Containing the following elements: MediaPlayerVlc: Instance; path.Path: Path of the temporary directory; unittest.case._LoggingWatcher: Captured output. """ if not config: config = { "kara_folder": self.kara_folder, "fullscreen": self.fullscreen, "vlc": { "instance_parameters": self.instance_parameters, "media_parameters": self.media_parameters, "use_default_window": self.use_default_window, }, } with ExitStack() as stack: temp = stack.enter_context(TempDir()) vlc_player = stack.enter_context( MediaPlayerVlc(Event(), Queue(), config, temp, warn_long_exit=False)) output = stack.enter_context( self.assertLogs("dakara_player.media_player.vlc", "DEBUG")) vlc_player.load() yield vlc_player, temp, output if check_error: # display errors in queue if any if not vlc_player.errors.empty(): _, error, traceback = vlc_player.errors.get(5) error.with_traceback(traceback) raise error # assert no errors to fail test if any self.assertFalse(vlc_player.stop.is_set())
def test_feed(self, mocked_dakara_server_class): """Test to feed """ # create the mocks mocked_dakara_server_class.return_value.get_songs.return_value = [] # create the object with TempDir() as temp: # copy required files with path("tests.resources.media", "dummy.ass") as file: Path(file).copy(temp) with path("tests.resources.media", "dummy.mkv") as file: Path(file).copy(temp) config = {"server": {}, "kara_folder": str(temp)} feeder = DakaraFeeder(config, progress=False) # call the method with self.assertLogs("dakara_feeder.dakara_feeder", "DEBUG"): with self.assertLogs("dakara_base.progress_bar"): feeder.feed() # assert the mocked calls mocked_dakara_server_class.return_value.get_songs.assert_called_with() mocked_dakara_server_class.return_value.post_song.assert_called_with( [ { "title": "dummy", "filename": "dummy.mkv", "directory": "", "duration": 2.023, "has_instrumental": True, "artists": [], "works": [], "tags": [], "version": "", "detail": "", "detail_video": "", "lyrics": "Piyo!", } ] )
def test_list_directory(self): """Test to list a directory using test ressource dummy files """ # call the function with TempDir() as temp: # copy required files with path("tests.resources.media", "dummy.ass") as file: Path(file).copy(temp) with path("tests.resources.media", "dummy.mkv") as file: Path(file).copy(temp) with self.assertLogs("dakara_feeder.directory_lister", "DEBUG"): listing = list_directory(Path(temp)) # check the structure self.assertEqual(len(listing), 1) self.assertEqual( SongPaths(Path("dummy.mkv"), subtitle=Path("dummy.ass")), listing[0])
def run_one( self, idf: Union[Path, eppy_IDF, str], epw_file: Path, backup_strategy: str = "on_error", backup_dir: Path = "./backup", simulation_name: Optional[str] = None, custom_process: Optional[Callable[[Simulation], None]] = None, version_mismatch_action: str = "raise", extra_files: Optional[Sequence[str]] = None, ) -> Simulation: """Run an EnergyPlus simulation with the provided idf and weather file. The IDF can be either a filename or an eppy IDF object. This function is process safe (as opposite as the one available in `eppy`). Arguments: idf {Union[Path, eppy_IDF, str]} -- idf file as filename or eppy IDF object. epw_file {Path} -- Weather file emplacement. Keyword Arguments: backup_strategy {str} -- when to save the files generated by e+ (either"always", "on_error" or None) (default: {"on_error"}) backup_dir {Path} -- where to save the files generated by e+ (default: {"./backup"}) simulation_name {str, optional} -- The simulation name. A random will be generated if not provided. custom_process {Callable[[Simulation], None], optional} -- overwrite the simulation post - process. Used to customize how the EnergyPlus files are treated after the simulation, but before cleaning the folder. version_mismatch_action {str} -- should be either ["raise", "warn", "ignore"] (default: {"raise"}) Returns: Simulation -- the simulation object """ if simulation_name is None: simulation_name = generate_slug() if backup_strategy not in ["on_error", "always", None]: raise ValueError( "`backup_strategy` argument should be either 'on_error', 'always'" " or None.") backup_dir = Path(backup_dir) with TempDir(prefix="energyplus_run_", dir=self.temp_dir) as td: if extra_files is not None: for extra_file in extra_files: Path(extra_file).copy(td) if isinstance(idf, eppy_IDF): idf = idf.idfstr() idf_file = td / "eppy_idf.idf" with open(idf_file, "w") as idf_descriptor: idf_descriptor.write(idf) else: idf_file = idf if version_mismatch_action in ["raise", "warn"]: self.check_version_compat( idf_file, version_mismatch_action=version_mismatch_action) idf_file, epw_file = (Path(f).abspath() for f in (idf_file, epw_file)) with td: logger.debug((idf_file, epw_file, td)) if idf_file not in td.files(): idf_file.copy(td) epw_file.copy(td) sim = Simulation( simulation_name, self.eplus_bin, idf_file, epw_file, self.idd_file, working_dir=td, post_process=custom_process, ) try: sim.run() except (ProcessExecutionError, KeyboardInterrupt): if backup_strategy == "on_error": sim.backup(backup_dir) raise finally: if backup_strategy == "always": sim.backup(backup_dir) return sim
def test_cleaned_up_on_interrupt(self): with contextlib.suppress(KeyboardInterrupt): with TempDir() as d: raise KeyboardInterrupt() assert not d.exists()
def run(self): """Worker main method It sets up the different workers and uses them as context managers, which guarantee that their different clean methods will be called prorperly. Then it starts the polling thread and waits for the end. When `run` is called, the end can come for several reasons: * the main thread (who calls the worker thread) has caught a Ctrl+C from the user; * an exception has been raised within the `run` method (directly in the worker thread); * an exception has been raised within the polling thread. """ # get the different workers as context managers # ExitStack makes the management of multiple context managers simpler # This mechanism plus the use of Worker classes allow to gracelly end # the execution of any thread within the context manager. It guarantees # as well that on leaving this context manager, all cleanup tasks will # be executed. with ExitStack() as stack: # temporary directory tempdir = stack.enter_context(TempDir(suffix=".dakara")) # font loader font_loader = stack.enter_context(FontLoader()) font_loader.load() # media player media_player = stack.enter_context(self.get_media_player_class()( self.stop, self.errors, self.config["player"], tempdir)) media_player.load() # communication with the dakara HTTP server dakara_server_http = DakaraServerHTTPConnection( self.config["server"], endpoint_prefix="api/", mute_raise=True) dakara_server_http.authenticate() token_header = dakara_server_http.get_token_header() # communication with the dakara WebSocket server dakara_server_websocket = stack.enter_context( DakaraServerWebSocketConnection( self.stop, self.errors, self.config["server"], header=token_header, endpoint="ws/playlist/device/", )) # manager for the precedent workers dakara_manager = DakaraManager( # noqa F841 font_loader, media_player, dakara_server_http, dakara_server_websocket) # start the worker timer dakara_server_websocket.timer.start() # wait for stop event self.stop.wait()
def backend_to_check(backend): with TempDir() as d: refimgpath = d / "ref.bmp" fillscreen.init(refimgpath) _backend_check(backend, childprocess=True, refimgpath=refimgpath)
class TestBuild(unittest.TestCase): def setUp(self): self.dirname = path(pkg_resources.resource_filename(__name__, "")) self.build_dir = TempDir() os.environ["CHARM_HIDE_METRICS"] = 'true' os.environ["CHARM_LAYERS_DIR"] = self.dirname / "layers" os.environ["CHARM_INTERFACES_DIR"] = self.dirname / "interfaces" os.environ["CHARM_CACHE_DIR"] = self.build_dir / "_cache" os.environ.pop("JUJU_REPOSITORY", None) os.environ.pop("LAYER_PATH", None) os.environ.pop("INTERFACE_PATH", None) ifd = build.fetchers.LayerFetcher.LAYER_INDEX self.p_post = mock.patch('requests.post') self.p_post.start() # preserve the layer index between tests self.p_layer_index = mock.patch( 'charmtools.build.fetchers.' 'LayerFetcher.LAYER_INDEX', ifd) self.p_layer_index.start() def tearDown(self): self.build_dir.rmtree_p() self.p_post.stop() self.p_layer_index.stop() def test_invalid_layer(self): # Test that invalid metadata.yaml files get a BuildError exception. builder = build.Builder() builder.log_level = "DEBUG" builder.build_dir = self.build_dir builder.cache_dir = builder.build_dir / "_cache" builder.series = "trusty" builder.name = "invalid-charm" builder.charm = "layers/invalid-layer" builder.no_local_layers = False metadata = path("tests/layers/invalid-layer/metadata.yaml") try: with self.dirname: builder() self.fail('Expected Builder to throw an exception on invalid YAML') except BuildError as e: self.assertEqual( "Failed to process {0}. " "Ensure the YAML is valid".format(metadata.abspath()), str(e)) @mock.patch("argparse.ArgumentParser.parse_args") @mock.patch("charmtools.build.builder.proof") @mock.patch("charmtools.build.builder.Builder") def test_failed_proof(self, mBuilder, mproof, mparse_args): # Test that charm-proof failures get a BuildError exception. mproof.proof.return_value = ([], 200) try: build.builder.main() self.fail('Expected Builder to throw an exception on proof error') except SystemExit as e: self.assertEqual(e.code, 200) @mock.patch("charmtools.build.builder.Builder.plan_version") def test_tester_layer(self, pv): bu = build.Builder() bu.log_level = "WARNING" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/tester" bu.hide_metrics = True bu.report = False remove_layer_file = self.dirname / 'layers/tester/to_remove' remove_layer_file.touch() self.addCleanup(remove_layer_file.remove_p) with self.dirname: with mock.patch.object(build.builder, 'log') as log: with mock.patch.object(build.builder, 'repofinder') as rf: rf.get_recommended_repo.return_value = None bu() log.warn.assert_called_with( 'Please add a `repo` key to your layer.yaml, ' 'with a url from which your layer can be cloned.') log.warn.reset_mock() rf.get_recommended_repo.return_value = 'myrepo' bu() log.warn.assert_called_with( 'Please add a `repo` key to your layer.yaml, ' 'e.g. repo: myrepo') base = bu.target_dir self.assertTrue(base.exists()) # Confirm that copyright file of lower layers gets renamed # and copyright file of top layer doesn't get renamed tester_copyright = (base / "copyright").text() mysql_copyright_path = base / "copyright.layer-mysql" self.assertIn("Copyright of tester", tester_copyright) self.assertTrue(mysql_copyright_path.isfile()) # Verify ignore rules applied self.assertFalse((base / ".bzr").exists()) self.assertEqual((base / "ignore").text(), "mysql\n") self.assertEqual((base / "exclude").text(), "test-base\n") self.assertEqual((base / "override-ignore").text(), "tester\n") self.assertEqual((base / "override-exclude").text(), "tester\n") self.assertFalse((base / "tests/00-setup").exists()) self.assertFalse((base / "tests/15-configs").exists()) self.assertTrue((base / "tests/20-deploy").exists()) actions = yaml.load((base / "actions.yaml").text()) resources = yaml.load((base / "resources.yaml").text()) self.assertNotIn("test-base", actions) self.assertIn("mysql", actions) self.assertIn("tester", actions) self.assertIn("test-base", resources) self.assertNotIn("mysql", resources) self.assertIn("tester", resources) # Metadata should have combined provides fields metadata = base / "metadata.yaml" self.assertTrue(metadata.exists()) metadata_data = yaml.load(metadata.open()) self.assertIn("shared-db", metadata_data['provides']) self.assertIn("storage", metadata_data['provides']) # The maintainer, maintainers values should only be from the top layer. self.assertIn("maintainer", metadata_data) self.assertEqual(metadata_data['maintainer'], "Tester <*****@*****.**>") self.assertNotIn("maintainers", metadata_data) # The tags list must be de-duplicated. self.assertEqual(metadata_data['tags'], ["databases"]) self.assertEqual(metadata_data['series'], ['xenial', 'trusty']) # Config should have keys but not the ones in deletes config = base / "config.yaml" self.assertTrue(config.exists()) config_data = yaml.load(config.open())['options'] self.assertIn("bind-address", config_data) self.assertNotIn("vip", config_data) self.assertIn("key", config_data) self.assertEqual(config_data["key"]["default"], None) # Issue #99 where strings lose their quotes in a charm build. self.assertIn("numeric-string", config_data) default_value = config_data['numeric-string']['default'] self.assertEqual(default_value, "0123456789", "value must be a string") # Issue 218, ensure proper order of layer application self.assertEqual(config_data['backup_retention_count']['default'], 7, 'Config from layers was merged in wrong order') cyaml = base / "layer.yaml" self.assertTrue(cyaml.exists()) cyaml_data = yaml.load(cyaml.open()) self.assertEquals(cyaml_data['includes'], ['layers/test-base', 'layers/mysql']) self.assertEquals(cyaml_data['is'], 'foo') self.assertEquals(cyaml_data['options']['mysql']['qux'], 'one') self.assertTrue((base / "hooks/config-changed").exists()) # Files from the top layer as overrides start = base / "hooks/start" self.assertTrue(start.exists()) self.assertIn("Overridden", start.text()) # Standard hooks generated from template stop = base / "hooks/stop" self.assertTrue(stop.exists()) self.assertIn("Hook: ", stop.text()) self.assertTrue((base / "README.md").exists()) self.assertEqual("dynamic tactics", (base / "README.md").text()) self.assertTrue((base / "old_tactic").exists()) self.assertEqual("processed", (base / "old_tactic").text()) sigs = base / ".build.manifest" self.assertTrue(sigs.exists()) data = json.load(sigs.open()) self.assertEquals(data['signatures']["README.md"], [ u'foo', "static", u'cfac20374288c097975e9f25a0d7c81783acdbc81' '24302ff4a731a4aea10de99' ]) self.assertEquals(data["signatures"]['metadata.yaml'], [ u'foo', "dynamic", u'03fc06a5e698e624231b826f4c47a60d3251cbc968fc1183ada444ca09b29ea6' ]) storage_attached = base / "hooks/data-storage-attached" storage_detaching = base / "hooks/data-storage-detaching" self.assertTrue(storage_attached.exists()) self.assertTrue(storage_detaching.exists()) self.assertIn("Hook: data", storage_attached.text()) self.assertIn("Hook: data", storage_detaching.text()) # confirm that files removed from a base layer get cleaned up self.assertTrue((base / 'to_remove').exists()) remove_layer_file.remove() with self.dirname: bu() self.assertFalse((base / 'to_remove').exists()) @mock.patch("charmtools.build.builder.Builder.plan_version") @responses.activate def test_remote_interface(self, pv): # XXX: this test does pull the git repo in the response responses.add(responses.GET, "https://juju.github.io/layer-index/" "interfaces/pgsql.json", body='''{ "id": "pgsql", "name": "pgsql4", "repo": "https://github.com/bcsaller/juju-relation-pgsql.git", "summary": "Postgres interface" }''', content_type="application/json") bu = build.Builder() bu.log_level = "WARNING" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/c-reactive" bu.hide_metrics = True bu.report = False with self.dirname: bu() base = bu.target_dir self.assertTrue(base.exists()) # basics self.assertTrue((base / "a").exists()) self.assertTrue((base / "README.md").exists()) # show that we pulled the interface from github init = base / "hooks/relations/pgsql/__init__.py" self.assertTrue(init.exists()) main = base / "hooks/reactive/main.py" self.assertTrue(main.exists()) @mock.patch("charmtools.build.builder.Builder.plan_version") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") @responses.activate def test_remote_layer(self, mcall, ph, pi, pv): # XXX: this test does pull the git repo in the response responses.add(responses.GET, "https://juju.github.io/layer-index/" "layers/basic.json", body='''{ "id": "basic", "name": "basic", "repo": "https://git.launchpad.net/~bcsaller/charms/+source/basic", "summary": "Base layer for all charms" }''', content_type="application/json") bu = build.Builder() bu.log_level = "WARNING" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/use-layers" bu.hide_metrics = True bu.report = False # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() base = bu.target_dir self.assertTrue(base.exists()) # basics self.assertTrue((base / "README.md").exists()) # show that we pulled charmhelpers from the basic layer as well mcall.assert_called_with( ("pip3", "install", "--user", "--ignore-installed", mock.ANY), env=mock.ANY) @mock.patch("charmtools.build.builder.Builder.plan_version") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") def test_pypi_installer(self, mcall, ph, pi, pv): bu = build.Builder() bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() mcall.assert_called_with( ("pip3", "install", "--user", "--ignore-installed", mock.ANY), env=mock.ANY) @mock.patch( "charmtools.build.tactics.VersionTactic._try_to_get_current_sha", return_value="fake sha") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") def test_version_tactic_without_existing_version_file( self, mcall, ph, pi, get_sha): bu = build.Builder() bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # ensure no an existing version file version_file = bu.charm / 'version' version_file.remove_p() # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() self.assertEqual((bu.target_dir / 'version').text(), 'fake sha') @mock.patch("charmtools.build.tactics.VersionTactic.CMDS", (('does_not_exist_cmd', ''), )) @mock.patch("charmtools.build.tactics.InstallerTactic.trigger", classmethod(lambda *a: False)) @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") def test_version_tactic_missing_cmd(self, ph, pi): bu = build.Builder() bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # ensure no an existing version file version_file = bu.charm / 'version' version_file.remove_p() # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() assert not (bu.target_dir / 'version').exists() @mock.patch("charmtools.build.tactics.VersionTactic.read", return_value="sha1") @mock.patch( "charmtools.build.tactics.VersionTactic._try_to_get_current_sha", return_value="sha2") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") def test_version_tactic_with_existing_version_file(self, mcall, ph, pi, get_sha, read): bu = build.Builder() bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: with mock.patch.object(build.tactics, 'log') as log: bu() log.warn.assert_has_calls([ mock.call('version sha1 is out of update, ' 'new sha sha2 will be used!') ], any_order=True) self.assertEqual((bu.target_dir / 'version').text(), 'sha2') @mock.patch("charmtools.build.builder.Builder.plan_version") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("path.Path.rmtree_p") @mock.patch("tempfile.mkdtemp") @mock.patch("charmtools.utils.Process") def test_wheelhouse(self, Process, mkdtemp, rmtree_p, ph, pi, pv): mkdtemp.return_value = '/tmp' bu = build.Builder() bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/whlayer" bu.hide_metrics = True bu.report = False bu.wheelhouse_overrides = self.dirname / 'wh-over.txt' # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: with mock.patch("path.Path.mkdir_p"): with mock.patch("path.Path.files"): bu() Process.assert_any_call( ('bash', '-c', '. /tmp/bin/activate ;' ' pip3 download --no-binary :all: ' '-d /tmp -r ' + self.dirname / 'layers/whlayer/wheelhouse.txt')) Process.assert_any_call( ('bash', '-c', '. /tmp/bin/activate ;' ' pip3 download --no-binary :all: ' '-d /tmp -r ' + self.dirname / 'wh-over.txt')) @mock.patch.object(build.tactics, 'log') @mock.patch.object(build.tactics.YAMLTactic, 'read', lambda s: setattr(s, '_read', True)) def test_layer_options(self, log): entity = mock.MagicMock(name='entity') target = mock.MagicMock(name='target') config = mock.MagicMock(name='config') base_layer = mock.MagicMock(name='base_layer') base_layer.directory.name = 'layer-base' base_layer.name = 'base' base = build.tactics.LayerYAML(entity, target, base_layer, config) base.data = { 'defines': { 'foo': { 'type': 'string', 'default': 'FOO', 'description': "Don't set me, bro", }, 'bar': { 'enum': ['yes', 'no'], 'description': 'Go to the bar?', }, } } assert base.lint() self.assertEqual(base.data['options']['base']['foo'], 'FOO') top_layer = mock.MagicMock(name='top_layer') top_layer.directory.name = 'layer-top' top_layer.name = 'top' top = build.tactics.LayerYAML(entity, target, top_layer, config) top.data = { 'options': {}, 'defines': { 'qux': { 'type': 'boolean', 'default': False, 'description': "Don't set me, bro", }, } } assert top.lint() top.data['options'].update({'base': { 'bar': 'bah', }}) assert not top.lint() top.combine(base) assert not top.lint() log.error.assert_called_with('Invalid value for option %s: %s', 'base.bar', "'bah' is not one of ['yes', 'no']") log.error.reset_mock() top.data['options']['base']['bar'] = 'yes' assert top.lint() self.assertEqual(top.data['options'], { 'base': { 'foo': 'FOO', 'bar': 'yes', }, 'top': { 'qux': False, }, }) @mock.patch('charmtools.build.tactics.getargspec') @mock.patch('charmtools.utils.walk') def test_custom_tactics(self, mwalk, mgetargspec): def _layer(tactics): return mock.Mock(config=build.builder.BuildConfig( {'tactics': tactics}), directory=path('.'), url=tactics[0]) builder = build.builder.Builder() builder.build_dir = self.build_dir builder.cache_dir = builder.build_dir / "_cache" builder.charm = 'foo' layers = { 'layers': [ _layer(['first']), _layer(['second']), _layer(['third']), ] } builder.plan_layers(layers, {}) calls = [ call[1]['current_config'].tactics for call in mwalk.call_args_list ] self.assertEquals(calls, [ ['first'], ['second', 'first'], ['third', 'second', 'first'], ]) mgetargspec.return_value = mock.Mock(args=[1, 2, 3, 4]) current_config = mock.Mock(tactics=[ mock.Mock(name='1', **{'trigger.return_value': False}), mock.Mock(name='2', **{'trigger.return_value': False}), mock.Mock(name='3', **{'trigger.return_value': True}), ]) build.tactics.Tactic.get(mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), current_config, mock.Mock()) self.assertEquals([t.trigger.called for t in current_config.tactics], [True, True, True]) self.assertEquals([t.called for t in current_config.tactics], [False, False, True])
class TestBuild(unittest.TestCase): def setUp(self): self.dirname = path(pkg_resources.resource_filename(__name__, "")) self.build_dir = TempDir() os.environ["CHARM_HIDE_METRICS"] = 'true' os.environ["CHARM_LAYERS_DIR"] = self.dirname / "layers" os.environ["CHARM_INTERFACES_DIR"] = self.dirname / "interfaces" os.environ["CHARM_CACHE_DIR"] = self.build_dir / "_cache" os.environ.pop("JUJU_REPOSITORY", None) os.environ.pop("LAYER_PATH", None) os.environ.pop("INTERFACE_PATH", None) self.p_post = mock.patch('requests.post') self.p_post.start() def tearDown(self): self.build_dir.rmtree_p() self.p_post.stop() build.fetchers.LayerFetcher.restore_layer_indexes() def test_default_no_hide_metrics(self): # In the absence of environment variables or command-line options, # Builder.hide_metrics is false. os.environ.pop("CHARM_HIDE_METRICS", None) builder = build.Builder() self.assertFalse(builder.hide_metrics) def test_environment_hide_metrics(self): # Setting the environment variable CHARM_HIDE_METRICS to a non-empty # value causes Builder.hide_metrics to be true. os.environ["CHARM_HIDE_METRICS"] = 'true' builder = build.Builder() self.assertTrue(builder.hide_metrics) def test_invalid_layer(self): # Test that invalid metadata.yaml files get a BuildError exception. builder = build.Builder() builder.log_level = "DEBUG" builder.build_dir = self.build_dir builder.cache_dir = builder.build_dir / "_cache" builder.series = "trusty" builder.name = "invalid-charm" builder.charm = "layers/invalid-layer" builder.no_local_layers = False metadata = path("tests/layers/invalid-layer/metadata.yaml") try: with self.dirname: builder() self.fail('Expected Builder to throw an exception on invalid YAML') except BuildError as e: self.assertEqual( "Failed to process {0}. " "Ensure the YAML is valid".format(metadata.abspath()), str(e)) @mock.patch("argparse.ArgumentParser.parse_args") @mock.patch("charmtools.build.builder.proof") @mock.patch("charmtools.build.builder.Builder") def test_failed_proof(self, mBuilder, mproof, mparse_args): # Test that charm-proof failures get a BuildError exception. mproof.proof.return_value = ([], 200) mBuilder().charm_file = False try: build.builder.main() self.fail('Expected Builder to throw an exception on proof error') except SystemExit as e: self.assertEqual(e.code, 200) @mock.patch("charmtools.build.builder.Builder.plan_version") def test_tester_layer(self, pv): bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARNING" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/tester" bu.hide_metrics = True bu.report = False bu.charm_file = True remove_layer_file = self.dirname / 'layers/tester/to_remove' remove_layer_file.touch() charm_file = self.dirname / 'foo.charm' self.addCleanup(remove_layer_file.remove_p) self.addCleanup(charm_file.remove_p) with self.dirname: with mock.patch.object(build.builder, 'log') as log: with mock.patch.object(build.builder, 'repofinder') as rf: rf.get_recommended_repo.return_value = None bu() log.warn.assert_called_with( 'Please add a `repo` key to your layer.yaml, ' 'with a url from which your layer can be cloned.') log.warn.reset_mock() rf.get_recommended_repo.return_value = 'myrepo' bu() log.warn.assert_called_with( 'Please add a `repo` key to your layer.yaml, ' 'e.g. repo: myrepo') base = bu.target_dir self.assertTrue(base.exists()) self.assertTrue(charm_file.exists()) with zipfile.ZipFile(charm_file, 'r') as zip: assert 'metadata.yaml' in zip.namelist() # Confirm that copyright file of lower layers gets renamed # and copyright file of top layer doesn't get renamed tester_copyright = (base / "copyright").text() mysql_copyright_path = base / "copyright.layer-mysql" self.assertIn("Copyright of tester", tester_copyright) self.assertTrue(mysql_copyright_path.isfile()) # Verify ignore rules applied self.assertFalse((base / ".bzr").exists()) self.assertEqual((base / "ignore").text(), "mysql\n") self.assertEqual((base / "exclude").text(), "test-base\n") self.assertEqual((base / "override-ignore").text(), "tester\n") self.assertEqual((base / "override-exclude").text(), "tester\n") self.assertFalse((base / "tests/00-setup").exists()) self.assertFalse((base / "tests/15-configs").exists()) self.assertTrue((base / "tests/20-deploy").exists()) actions = yaml.safe_load((base / "actions.yaml").text()) resources = yaml.safe_load((base / "resources.yaml").text()) self.assertNotIn("test-base", actions) self.assertIn("mysql", actions) self.assertIn("tester", actions) self.assertIn("test-base", resources) self.assertNotIn("mysql", resources) self.assertIn("tester", resources) # Metadata should have combined provides fields metadata = base / "metadata.yaml" self.assertTrue(metadata.exists()) metadata_data = yaml.safe_load(metadata.open()) self.assertIn("shared-db", metadata_data['provides']) self.assertIn("storage", metadata_data['provides']) # The maintainer, maintainers values should only be from the top layer. self.assertIn("maintainer", metadata_data) self.assertEqual(metadata_data['maintainer'], b"T\xc3\xa9sty T\xc3\xa9st\xc3\xa9r " b"<t\xc3\xa9st\xc3\[email protected]>".decode('utf8')) self.assertNotIn("maintainers", metadata_data) # The tags list must be de-duplicated. self.assertEqual(metadata_data['tags'], ["databases"]) self.assertEqual(metadata_data['series'], ['xenial', 'trusty']) # Config should have keys but not the ones in deletes config = base / "config.yaml" self.assertTrue(config.exists()) config_data = yaml.safe_load(config.open())['options'] self.assertIn("bind-address", config_data) self.assertNotIn("vip", config_data) self.assertIn("key", config_data) self.assertEqual(config_data["key"]["default"], None) # Issue #99 where strings lose their quotes in a charm build. self.assertIn("numeric-string", config_data) default_value = config_data['numeric-string']['default'] self.assertEqual(default_value, "0123456789", "value must be a string") # Issue 218, ensure proper order of layer application self.assertEqual(config_data['backup_retention_count']['default'], 7, 'Config from layers was merged in wrong order') cyaml = base / "layer.yaml" self.assertTrue(cyaml.exists()) cyaml_data = yaml.safe_load(cyaml.open()) self.assertEquals(cyaml_data['includes'], ['layers/test-base', 'layers/mysql']) self.assertEquals(cyaml_data['is'], 'foo') self.assertEquals(cyaml_data['options']['mysql']['qux'], 'one') self.assertTrue((base / "hooks/config-changed").exists()) # Files from the top layer as overrides start = base / "hooks/start" self.assertTrue(start.exists()) self.assertIn("Overridden", start.text()) # Standard hooks generated from template stop = base / "hooks/stop" self.assertTrue(stop.exists()) self.assertIn("Hook: ", stop.text()) self.assertTrue((base / "README.md").exists()) self.assertEqual("dynamic tactics", (base / "README.md").text()) self.assertTrue((base / "old_tactic").exists()) self.assertEqual("processed", (base / "old_tactic").text()) sigs = base / ".build.manifest" self.assertTrue(sigs.exists()) data = json.load(sigs.open()) self.assertEquals(data['signatures']["README.md"], [ u'foo', "static", u'cfac20374288c097975e9f25a0d7c81783acdbc81' '24302ff4a731a4aea10de99']) self.assertEquals(data["signatures"]['metadata.yaml'], [ u'foo', "dynamic", u'12c1f6fc865da0660f6dc044cca03b0244e883d9a99fdbdfab6ef6fc2fed63b7' ]) storage_attached = base / "hooks/data-storage-attached" storage_detaching = base / "hooks/data-storage-detaching" self.assertTrue(storage_attached.exists()) self.assertTrue(storage_detaching.exists()) self.assertIn("Hook: data", storage_attached.text()) self.assertIn("Hook: data", storage_detaching.text()) # confirm that files removed from a base layer get cleaned up self.assertTrue((base / 'to_remove').exists()) remove_layer_file.remove() with self.dirname: bu() self.assertFalse((base / 'to_remove').exists()) @mock.patch("charmtools.build.builder.Builder.plan_version") @responses.activate def test_remote_interface(self, pv): # XXX: this test does pull the git repo in the response responses.add(responses.GET, "https://juju.github.io/layer-index/" "interfaces/pgsql.json", body='''{ "id": "pgsql", "name": "pgsql4", "repo": "https://github.com/bcsaller/juju-relation-pgsql.git", "summary": "Postgres interface" }''', content_type="application/json") bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARNING" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/c-reactive" bu.hide_metrics = True bu.report = False with self.dirname: bu() base = bu.target_dir self.assertTrue(base.exists()) # basics self.assertTrue((base / "a").exists()) self.assertTrue((base / "README.md").exists()) # show that we pulled the interface from github init = base / "hooks/relations/pgsql/__init__.py" self.assertTrue(init.exists()) main = base / "hooks/reactive/main.py" self.assertTrue(main.exists()) @mock.patch("charmtools.build.builder.Builder.plan_version") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") @responses.activate def test_remote_layer(self, mcall, ph, pi, pv): # XXX: this test does pull the git repo in the response responses.add(responses.GET, "https://juju.github.io/layer-index/" "layers/basic.json", body='''{ "id": "basic", "name": "basic", "repo": "https://git.launchpad.net/~bcsaller/charms/+source/basic", "summary": "Base layer for all charms" }''', content_type="application/json") bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARNING" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/use-layers" bu.hide_metrics = True bu.report = False # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() base = bu.target_dir self.assertTrue(base.exists()) # basics self.assertTrue((base / "README.md").exists()) # show that we pulled charmhelpers from the basic layer as well mcall.assert_called_with(("pip3", "install", "--user", "--ignore-installed", mock.ANY), env=mock.ANY) @mock.patch("charmtools.build.builder.Builder.plan_version") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") def test_pypi_installer(self, mcall, ph, pi, pv): bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() mcall.assert_called_with(("pip3", "install", "--user", "--ignore-installed", mock.ANY), env=mock.ANY) @mock.patch( "charmtools.build.tactics.VersionTactic._try_to_get_current_sha", return_value="fake sha") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") def test_version_tactic_without_existing_version_file(self, mcall, ph, pi, get_sha): bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # ensure no an existing version file version_file = bu.charm / 'version' version_file.remove_p() # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() self.assertEqual((bu.target_dir / 'version').text(), 'fake sha') @mock.patch("charmtools.build.tactics.VersionTactic.CMDS", ( ('does_not_exist_cmd', ''), )) @mock.patch("charmtools.build.tactics.InstallerTactic.trigger", classmethod(lambda *a: False)) @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") def test_version_tactic_missing_cmd(self, ph, pi): bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # ensure no an existing version file version_file = bu.charm / 'version' version_file.remove_p() # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: bu() assert not (bu.target_dir / 'version').exists() @mock.patch("charmtools.build.tactics.VersionTactic.read", return_value="sha1") @mock.patch( "charmtools.build.tactics.VersionTactic._try_to_get_current_sha", return_value="sha2") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("charmtools.utils.Process") def test_version_tactic_with_existing_version_file(self, mcall, ph, pi, get_sha, read): bu = build.Builder() bu.ignore_lock_file = True bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "foo" bu.charm = "layers/chlayer" bu.hide_metrics = True bu.report = False # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: with mock.patch.object(build.tactics, 'log') as log: bu() log.warn.assert_has_calls( [mock.call('version sha1 is out of update, ' 'new sha sha2 will be used!')], any_order=True) self.assertEqual((bu.target_dir / 'version').text(), 'sha2') @mock.patch("charmtools.utils.sign") @mock.patch("charmtools.build.builder.Builder.plan_version") @mock.patch("charmtools.build.builder.Builder.plan_interfaces") @mock.patch("charmtools.build.builder.Builder.plan_hooks") @mock.patch("path.Path.rmtree_p") @mock.patch("tempfile.mkdtemp") @mock.patch("charmtools.utils.Process") def test_wheelhouse(self, Process, mkdtemp, rmtree_p, ph, pi, pv, sign): build.tactics.WheelhouseTactic.per_layer = False mkdtemp.return_value = '/tmp' bu = build.Builder() bu.log_level = "WARN" bu.build_dir = self.build_dir bu.cache_dir = bu.build_dir / "_cache" bu.series = "trusty" bu.name = "whlayer" bu.charm = "layers/whlayer" bu.hide_metrics = True bu.report = False bu.wheelhouse_overrides = self.dirname / 'wh-over.txt' def _store_wheelhouses(args): filename = args[-1].split()[-1] if filename.endswith('.txt'): Process._wheelhouses.append(path(filename).lines(retain=False)) return mock.Mock(return_value=mock.Mock(exit_code=0)) Process._wheelhouses = [] Process.side_effect = _store_wheelhouses # remove the sign phase bu.PHASES = bu.PHASES[:-2] with self.dirname: with mock.patch("path.Path.mkdir_p"): with mock.patch("path.Path.files"): bu() self.assertEqual(len(Process._wheelhouses), 1) # note that setuptools uses both hyphen and underscore, but # that should be normalized so that they match self.assertEqual(Process._wheelhouses[0], [ '# layers/whbase', '# base-comment', '# foo==1.0 # overridden by whlayer', '# bar==1.0 # overridden by whlayer', '# qux==1.0 # overridden by whlayer', '# setuptools-scm<=1.17.0 # overridden by ' '--wheelhouse-overrides', '', '# whlayer', '# git+https://github.com/me/baz#egg=baz # comment', 'foo==2.0', 'git+https://github.com/me/bar#egg=bar', '# qux==2.0 # overridden by --wheelhouse-overrides', '', '# --wheelhouse-overrides', 'git+https://github.com/me/qux#egg=qux', 'setuptools_scm>=3.0<=3.4.1', '', ]) sign.return_value = 'signature' wh = build.tactics.WheelhouseTactic(path('wheelhouse.txt'), mock.Mock(directory=path('wh')), mock.Mock(url='charm'), mock.Mock()) # package name gets normalized properly when checking _layer_refs wh._layer_refs['setuptools-scm'] = 'layer:foo' wh.tracked = {path('wh/setuptools_scm-1.17.0.tar.gz')} self.assertEqual(wh.sign(), { 'wheelhouse.txt': ('charm', 'dynamic', 'signature'), 'setuptools_scm-1.17.0.tar.gz': ('layer:foo', 'dynamic', 'signature'), }) @mock.patch.object(build.tactics, 'path') def test_wheelhouse_missing_package_name(self, path): wh = build.tactics.WheelhouseTactic(mock.Mock(name='entity'), mock.Mock(name='target'), mock.Mock(name='layer', url='foo'), mock.Mock(name='next_config')) path().text.return_value = 'https://example.com/my-package' with self.assertRaises(BuildError): wh.read() path().text.return_value = 'https://example.com/my-package#egg=foo' wh.read() self.assertIn('foo', wh._layer_refs.keys()) @mock.patch.object(build.tactics, 'log') @mock.patch.object(build.tactics.YAMLTactic, 'read', lambda s: setattr(s, '_read', True)) def test_layer_options(self, log): entity = mock.MagicMock(name='entity') target = mock.MagicMock(name='target') config = mock.MagicMock(name='config') base_layer = mock.MagicMock(name='base_layer') base_layer.directory.name = 'layer-base' base_layer.name = 'base' base = build.tactics.LayerYAML(entity, target, base_layer, config) base.data = { 'defines': { 'foo': { 'type': 'string', 'default': 'FOO', 'description': "Don't set me, bro", }, 'bar': { 'enum': ['yes', 'no'], 'description': 'Go to the bar?', }, } } assert base.lint() self.assertEqual(base.data['options']['base']['foo'], 'FOO') top_layer = mock.MagicMock(name='top_layer') top_layer.directory.name = 'layer-top' top_layer.name = 'top' top = build.tactics.LayerYAML(entity, target, top_layer, config) top.data = { 'options': { }, 'defines': { 'qux': { 'type': 'boolean', 'default': False, 'description': "Don't set me, bro", }, } } assert top.lint() top.data['options'].update({ 'base': { 'bar': 'bah', } }) assert not top.lint() top.combine(base) assert not top.lint() log.error.assert_called_with('Invalid value for option %s: %s', 'base.bar', "'bah' is not one of ['yes', 'no']") log.error.reset_mock() top.data['options']['base']['bar'] = 'yes' assert top.lint() self.assertEqual(top.data['options'], { 'base': { 'foo': 'FOO', 'bar': 'yes', }, 'top': { 'qux': False, }, }) @mock.patch('charmtools.build.tactics.getargspec') @mock.patch('charmtools.utils.walk') def test_custom_tactics(self, mwalk, mgetargspec): def _layer(tactics): return mock.Mock(config=build.builder.BuildConfig({'tactics': tactics}), directory=path('.'), url=tactics[0]) builder = build.builder.Builder() builder.ignore_lock_file = True builder.build_dir = self.build_dir builder.cache_dir = builder.build_dir / "_cache" builder.charm = 'foo' layers = {'layers': [ _layer(['first']), _layer(['second']), _layer(['third']), ]} builder.plan_layers(layers, {}) calls = [call[1]['current_config'].tactics for call in mwalk.call_args_list] self.assertEquals(calls, [ ['first'], ['second', 'first'], ['third', 'second', 'first'], ]) mgetargspec.return_value = mock.Mock(args=[1, 2, 3, 4]) current_config = mock.Mock(tactics=[ mock.Mock(name='1', **{'trigger.return_value': False}), mock.Mock(name='2', **{'trigger.return_value': False}), mock.Mock(name='3', **{'trigger.return_value': True}), ]) build.tactics.Tactic.get(mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), current_config, mock.Mock()) self.assertEquals([t.trigger.called for t in current_config.tactics], [True, True, True]) self.assertEquals([t.called for t in current_config.tactics], [False, False, True])