Пример #1
0
 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()
Пример #2
0
 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()
Пример #3
0
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)
Пример #4
0
 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()
Пример #5
0
 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()
Пример #6
0
    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)
Пример #7
0
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())
Пример #9
0
    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())
Пример #10
0
    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())
Пример #11
0
    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)
Пример #12
0
    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,
        }
Пример #13
0
    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
Пример #14
0
 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()
Пример #15
0
    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()
Пример #16
0
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()
Пример #17
0
    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)
Пример #18
0
 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()
Пример #19
0
 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()
Пример #20
0
    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())
Пример #21
0
 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()
Пример #22
0
 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()
Пример #23
0
 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_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])
Пример #26
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
Пример #27
0
    def test_cleaned_up_on_interrupt(self):
        with contextlib.suppress(KeyboardInterrupt):
            with TempDir() as d:
                raise KeyboardInterrupt()

        assert not d.exists()
Пример #28
0
    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()
Пример #29
0
def backend_to_check(backend):
    with TempDir() as d:
        refimgpath = d / "ref.bmp"
        fillscreen.init(refimgpath)
        _backend_check(backend, childprocess=True, refimgpath=refimgpath)
Пример #30
0
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])
Пример #31
0
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])