# -*- coding: utf-8 -*-
import unittest
import sys
import os
import warnings
from os.path import basename
from os.path import dirname
from os.path import exists
from os.path import isfile
from os.path import join
from os.path import normcase
from types import ModuleType

from pkg_resources import Distribution
from pkg_resources import EntryPoint
from pkg_resources import WorkingSet
from pkg_resources import safe_name

from calmjs import artifact
from calmjs import dist
from calmjs.utils import pretty_logging
from calmjs.dist import find_pkg_dist
from calmjs.registry import get
from calmjs.artifact import ArtifactBuilder
from calmjs.artifact import ArtifactRegistry
from calmjs.artifact import extract_builder_result
from calmjs.artifact import prepare_export_location
from calmjs.artifact import trace_toolchain
from calmjs.artifact import verify_builder
from calmjs.types.exceptions import ToolchainAbort
from calmjs.toolchain import NullToolchain
from calmjs.toolchain import Spec

from calmjs.testing import utils
from calmjs.testing import mocks
from calmjs.testing.artifact import generic_builder


class IntegrationTestCase(unittest.TestCase):

    def test_integrated_get(self):
        # test that the default registry is registered.
        self.assertTrue(isinstance(get('calmjs.artifacts'), ArtifactRegistry))


class UtilsTestCase(unittest.TestCase):

    def test_prepare_base(self):
        basedir = utils.mkdtemp(self)
        export_target = join(basedir, 'artifacts', 'export.js')
        with pretty_logging(stream=mocks.StringIO()) as s:
            self.assertTrue(prepare_export_location(export_target))

        self.assertTrue(exists(join(basedir, 'artifacts')))
        self.assertIn("artifacts", s.getvalue())

    def test_prepare_base_parent_is_file(self):
        basedir = utils.mkdtemp(self)
        export_target = join(basedir, 'artifacts', 'export.js')
        with open(join(basedir, 'artifacts'), 'w'):
            pass

        with self.assertRaises(ToolchainAbort):
            with pretty_logging(stream=mocks.StringIO()) as s:
                self.assertFalse(prepare_export_location(export_target))

        self.assertIn("cannot export to '%s'" % export_target, s.getvalue())
        self.assertTrue(isfile(join(basedir, 'artifacts')))

    def test_prepare_existed_file_removed(self):
        basedir = utils.mkdtemp(self)
        export_target = join(basedir, 'export.js')
        with open(export_target, 'w'):
            pass

        with pretty_logging(stream=mocks.StringIO()) as s:
            self.assertTrue(prepare_export_location(export_target))

        self.assertIn(
            "removing existing export target at '%s'" % export_target,
            s.getvalue())
        self.assertFalse(exists(export_target))

    def test_prepare_existed_dir_removed(self):
        basedir = utils.mkdtemp(self)
        export_target = join(basedir, 'export.js')
        os.mkdir(export_target)

        with pretty_logging(stream=mocks.StringIO()) as s:
            self.assertTrue(prepare_export_location(export_target))

        self.assertIn(
            "removing existing export target directory at '%s'" %
            export_target, s.getvalue())
        self.assertFalse(exists(export_target))

    def test_prepare_existed_dir_collision(self):
        basedir = utils.mkdtemp(self)
        conflict = join(basedir, 'some')
        export_target = join(conflict, 'target', 'export.js')
        with open(conflict, 'w'):
            pass

        with self.assertRaises(ToolchainAbort):
            with pretty_logging(stream=mocks.StringIO()) as s:
                self.assertFalse(prepare_export_location(export_target))

        self.assertIn("failed to prepare export location", s.getvalue())
        self.assertFalse(exists(export_target))

    def test_verify_builder(self):
        def good_builder(package_names, export_target):
            "An expected argument signature"

        def bad_builder(package_names):
            "An unexpected argument signature"

        self.assertTrue(verify_builder(good_builder))
        self.assertFalse(verify_builder(bad_builder))

    def test_extract_builder_result(self):
        self.assertEqual(2, len(
            extract_builder_result((NullToolchain(), Spec(),))))
        self.assertEqual((None, None), extract_builder_result(
            (Spec(), NullToolchain(),)))
        self.assertEqual((None, None), extract_builder_result(
            (NullToolchain(), None,)))
        self.assertEqual((None, None), extract_builder_result(None))

    def test_trace_toolchain(self):
        version = find_pkg_dist('calmjs').version
        results = trace_toolchain(NullToolchain())
        self.assertEqual(results, [{
            'calmjs.toolchain:NullToolchain': {
                'project_name': 'calmjs',
                'version': version,
            },
        }, {
            'calmjs.toolchain:Toolchain': {
                'project_name': 'calmjs',
                'version': version,
            }
        }])


class ArtifactRegistryTestCase(unittest.TestCase):
    """
    Standard test cases.
    """

    def assertPathsEqual(self, first, second):
        def norm(items):
            return [normcase(i) for i in items]
        self.assertEqual(norm(first), norm(second))

    def test_basic(self):
        working_dir = utils.mkdtemp(self)

        utils.make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
            ])),
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'full.js = calmjs_testbuild:full',
                'base.lib.js = calmjs_testbuild:lib',
            ])),
        ), 'base', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'base',
            ])),
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'full.js = calmjs_testbuild:full',
                'lib1.lib.js = calmjs_testbuild:lib',
            ])),
        ), 'lib1', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'base',
            ])),
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'full.js = calmjs_testbuild:full',
                'lib2.lib.js = calmjs_testbuild_extended:lib',
            ])),
        ), 'lib2', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'lib1',
                'lib2',
            ])),
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'full.js = calmjs_testbuild:full',
                # this one doesn't provided a standalone library
            ])),
        ), 'app1', '1.0', working_dir=working_dir)

        mock_ws = WorkingSet([working_dir])

        # stub the default working set in calmjs.dist for the resolver
        # to work.
        utils.stub_item_attr_value(self, dist, 'default_working_set', mock_ws)
        # still specify the working set.
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)

        self.assertNotEqual(len(list(registry.iter_records())), 0)

        self.assertEqual(
            normcase(join(
                working_dir, 'lib1-1.0.egg-info', 'calmjs_artifacts',
                'lib1.lib.js'
            )),
            normcase(registry.get_artifact_filename('lib1', 'lib1.lib.js')),
        )

        self.assertEqual([], list(registry.resolve_artifacts_by_builder_compat(
            ['no_such_package'], 'full')))
        self.assertEqual([], list(registry.resolve_artifacts_by_builder_compat(
            ['lib1'], 'no_such_rule')))

        self.assertPathsEqual([
            join(working_dir, 'lib1-1.0.egg-info', 'calmjs_artifacts',
                 'full.js'),
        ], list(registry.resolve_artifacts_by_builder_compat(
            ['lib1'], 'full')))

        self.assertPathsEqual([
            join(working_dir, 'base-1.0.egg-info', 'calmjs_artifacts',
                 'base.lib.js'),
            join(working_dir, 'lib1-1.0.egg-info', 'calmjs_artifacts',
                 'lib1.lib.js'),
        ], list(registry.resolve_artifacts_by_builder_compat(
            ['lib1'], 'lib', dependencies=True)))

        self.assertPathsEqual([
            join(working_dir, 'base-1.0.egg-info', 'calmjs_artifacts',
                 'base.lib.js'),
            join(working_dir, 'lib2-1.0.egg-info', 'calmjs_artifacts',
                 'lib2.lib.js'),
            join(working_dir, 'lib1-1.0.egg-info', 'calmjs_artifacts',
                 'lib1.lib.js'),
        ], list(registry.resolve_artifacts_by_builder_compat(
            ['lib2', 'lib1'], 'lib', dependencies=True)))

        self.assertPathsEqual([
            join(working_dir, 'base-1.0.egg-info', 'calmjs_artifacts',
                 'base.lib.js'),
            join(working_dir, 'lib1-1.0.egg-info', 'calmjs_artifacts',
                 'lib1.lib.js'),
            join(working_dir, 'lib2-1.0.egg-info', 'calmjs_artifacts',
                 'lib2.lib.js'),
        ], list(registry.resolve_artifacts_by_builder_compat(
            ['app1'], 'lib', dependencies=True)))

        entry_point = registry.belongs_to(join(
            working_dir, 'base-1.0.egg-info', 'calmjs_artifacts',
            'base.lib.js',
        ))
        self.assertEqual('base', entry_point.dist.project_name)
        self.assertEqual('base.lib.js', entry_point.name)

    def test_conflict_registration(self):
        # create an empty working set for a clean-slate test.
        cwd = utils.mkdtemp(self)
        mock_ws = WorkingSet([])
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)
        # using named case for case sensitivity test.
        st = join(cwd, 'calmjs_artifacts', 'Simple.js')
        dist_ = Distribution(cwd, project_name='pkg', version='1.0')
        dist_.egg_info = cwd  # just lazy
        s1 = EntryPoint.parse('Simple.js = dummy_builder:builder1')
        s1.dist = dist_
        s2 = EntryPoint.parse('Simple.js = dummy_builder:builder2')
        s2.dist = dist_

        with pretty_logging(stream=mocks.StringIO()) as stream:
            registry.register_entry_point(s1)
            # normal registry usage shouldn't be able to do this.
            registry.register_entry_point(s2)

        log = stream.getvalue()
        self.assertIn(
            "entry point 'Simple.js = dummy_builder:builder2' from package "
            "'pkg 1.0' resolves to the path '%s' which was already "
            "registered to entry point 'Simple.js = dummy_builder:builder1'; "
            "conflicting entry point registration will be ignored." % st,
            log
        )

    def test_denormalized_package_names(self):
        working_dir = utils.mkdtemp(self)
        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'full.js = calmjs_testbuild:full',
            ])),
        ), 'de_normal_name', '1.0', working_dir=working_dir)

        mock_ws = WorkingSet([working_dir])
        # stub the default working set in calmjs.dist for the resolver
        # to work.
        utils.stub_item_attr_value(self, dist, 'default_working_set', mock_ws)
        # still specify the working set.
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)
        self.assertEqual(
            1, len(list(registry.iter_records_for('de_normal_name'))))
        # also test internal consistency
        self.assertIn('de_normal_name', registry.compat_builders['full'])
        self.assertIn('de_normal_name', registry.packages)
        default = registry.get_artifact_filename('de_normal_name', 'full.js')
        normal = registry.get_artifact_filename(
            safe_name('de_normal_name'), 'full.js')
        self.assertEqual(default, normal)

    def test_normcase_registration(self):
        # create an empty working set for a clean-slate test.
        cwd = utils.mkdtemp(self)
        mock_ws = WorkingSet([])
        dist_ = Distribution(cwd, project_name='pkg', version='1.0')
        dist_.egg_info = cwd  # just lazy
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)
        # case sensitive test; have to patch the normcase at artifact
        # module with the nt version
        from ntpath import normcase as nt_normcase
        utils.stub_item_attr_value(self, artifact, 'normcase', nt_normcase)
        # using named case for case sensitivity test.
        c1 = EntryPoint.parse('case.js = dummy_builder:builder1')
        c1.dist = dist_
        c2 = EntryPoint.parse('Case.js = dummy_builder:builder2')
        c2.dist = dist_
        # use the error one
        ct = join(cwd, 'calmjs_artifacts', 'Case.js')
        with pretty_logging(stream=mocks.StringIO()) as stream:
            registry.register_entry_point(c1)
            registry.register_entry_point(c2)

        log = stream.getvalue()
        self.assertIn(
            "entry point 'Case.js = dummy_builder:builder2' from package "
            "'pkg 1.0' resolves to the path '%s' which was already "
            "registered to entry point 'case.js = dummy_builder:builder1'; "
            "conflicting entry point registration will be ignored." % ct,
            log
        )
        self.assertIn(
            "the file mapping error is caused by this platform's case-"
            "insensitive filename", log
        )

    def test_update_artifact_metadata(self):
        # inject dummy module and add cleanup
        mod = ModuleType('calmjs_testing_dummy')
        mod.complete = generic_builder
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)
        utils.make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'calmjs',
            ])),
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'artifact.js = calmjs_testing_dummy:complete',
            ])),
        ), 'app', '1.0', working_dir=working_dir)
        # mock a version of calmjs within that environment too
        utils.make_dummy_dist(self, (
            ('entry_points.txt', ''),
        ), 'calmjs', '1.0', working_dir=working_dir)

        mock_ws = WorkingSet([working_dir])
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)
        registry.update_artifact_metadata('app', {})
        self.assertTrue(exists(registry.metadata.get('app')))

        with pretty_logging(stream=mocks.StringIO()) as s:
            registry.update_artifact_metadata('calmjs', {})
        self.assertIn(
            "package 'calmjs' has not declare any artifacts", s.getvalue())

    def test_iter_builders_side_effect(self):
        # inject dummy module and add cleanup
        mod = ModuleType('calmjs_testing_dummy')
        mod.complete = generic_builder
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)
        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'artifact.js = calmjs_testing_dummy:complete',
            ])),
        ), 'app', '1.0', working_dir=working_dir)
        mock_ws = WorkingSet([working_dir])
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)
        registry.update_artifact_metadata('app', {})

        root = join(working_dir, 'app-1.0.egg-info', 'calmjs_artifacts')
        self.assertFalse(exists(root))
        ep, toolchain, spec = next(registry.iter_builders_for('app'))
        self.assertFalse(exists(root))
        # directory only created after the toolchain is executed
        toolchain(spec)
        self.assertTrue(exists(root))

    def test_iter_builders_side_effect_build_issue(self):
        mod = ModuleType('calmjs_testing_dummy')
        mod.complete = generic_builder
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)
        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'artifact.js = calmjs_testing_dummy:complete',
            ])),
        ), 'app', '1.0', working_dir=working_dir)
        mock_ws = WorkingSet([working_dir])
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)
        registry.update_artifact_metadata('app', {})

        root = join(working_dir, 'app-1.0.egg-info', 'calmjs_artifacts')
        # clog the build directory so build cannot happen
        with open(join(root), 'w'):
            pass

        ep, toolchain, spec = next(registry.iter_builders_for('app'))
        check = []
        spec.advise('after_prepare', check.append, True)
        with pretty_logging(stream=mocks.StringIO()) as stream:
            with self.assertRaises(ToolchainAbort):
                toolchain(spec)
        self.assertIn(
            "an advice in group 'before_prepare' triggered an abort",
            stream.getvalue())
        # should have stopped at before_prepare
        self.assertFalse(check)

    def test_iter_builders_verify_export_target_legacy(self):
        mod = ModuleType('calmjs_testing_dummy')
        mod.complete = generic_builder
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)
        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'artifact.js = calmjs_testing_dummy:complete',
            ])),
        ), 'app', '1.0', working_dir=working_dir)
        mock_ws = WorkingSet([working_dir])
        root = join(working_dir, 'app-1.0.egg-info', 'calmjs_artifacts')
        targets = []

        def check_target(export_target):
            # manually create the root here, to allow the completion
            # of the build process by doing what an existing
            # implementation that reuse existing code might have done.
            targets.append(export_target)
            prepare_export_location(export_target)

        # ensure that the build directory exists, because the default

        class FakeArtifactRegistry(ArtifactRegistry):
            def verify_export_target(self, export_target):
                return check_target

        registry = FakeArtifactRegistry(
            'calmjs.artifacts', _working_set=mock_ws)

        # the invalid.js should be filtered out
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter('always')
            self.assertEqual(1, len(list(registry.iter_builders_for('app'))))

        self.assertIn(
            "FakeArtifactRegistry.verify_export_target returned a callable",
            str(w[-1].message))

        # ensure no early execution of check_target
        self.assertFalse(targets)
        self.assertFalse(exists(root))
        with warnings.catch_warnings(record=True):
            with pretty_logging(stream=mocks.StringIO()):
                registry.process_package('app')
        # ensure no early execution of check_target
        self.assertTrue(targets)
        self.assertTrue(exists(root))

    def test_iter_builders_verify_export_target(self):
        mod = ModuleType('calmjs_testing_dummy')
        mod.complete = generic_builder
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)
        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'artifact.js = calmjs_testing_dummy:complete',
                'invalid.js = calmjs_testing_dummy:complete',
            ])),
        ), 'app', '1.0', working_dir=working_dir)
        mock_ws = WorkingSet([working_dir])

        class FakeArtifactRegistry(ArtifactRegistry):
            def verify_export_target(self, export_target):
                return 'invalid.js' not in export_target

        registry = FakeArtifactRegistry(
            'calmjs.artifacts', _working_set=mock_ws)

        # the invalid.js should be filtered out
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.assertEqual(1, len(list(registry.iter_builders_for('app'))))
        self.assertIn("invalid.js' has been rejected", stream.getvalue())

    def test_build_artifacts_success(self):
        # inject dummy module and add cleanup
        mod = ModuleType('calmjs_testing_dummy')
        mod.extra = generic_builder
        mod.complete = generic_builder
        mod.partial = generic_builder
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)

        utils.make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'calmjs',
            ])),
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'artifact.js = calmjs_testing_dummy:complete',
                'partial.js = calmjs_testing_dummy:partial',
            ])),
        ), 'app', '1.0', working_dir=working_dir)

        # mock a version of calmjs within that environment too
        utils.make_dummy_dist(self, (
            ('entry_points.txt', ''),
        ), 'calmjs', '1.0', working_dir=working_dir)

        def version(bin_path, version_flag='-v', kw={}):
            return '0.0.0'

        mock_ws = WorkingSet([working_dir])
        utils.stub_item_attr_value(self, dist, 'default_working_set', mock_ws)
        utils.stub_item_attr_value(
            self, artifact, 'get_bin_version_str', version)
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)

        # quick check of the artifact metadata beforehand
        self.assertEqual({}, registry.get_artifact_metadata('app'))

        registry.process_package('app')
        complete = list(registry.resolve_artifacts_by_builder_compat(
            ['app'], 'complete'))
        partial = list(registry.resolve_artifacts_by_builder_compat(
            ['app'], 'partial'))

        self.assertEqual(len(complete), 1)
        self.assertEqual(len(partial), 1)
        self.assertEqual(basename(complete[0]), 'artifact.js')
        self.assertEqual(basename(partial[0]), 'partial.js')

        with open(complete[0]) as fd:
            self.assertEqual(fd.read(), 'app')

        with open(partial[0]) as fd:
            self.assertEqual(fd.read(), 'app')

        self.assertEqual({
            'calmjs_artifacts': {
                'artifact.js': {
                    'builder': 'calmjs_testing_dummy:complete',
                    'toolchain_bases': [
                        {'calmjs.testing.artifact:ArtifactToolchain': {
                            'project_name': 'calmjs',
                            'version': '1.0',
                        }},
                        {'calmjs.toolchain:NullToolchain': {
                            'project_name': 'calmjs',
                            'version': '1.0',
                        }},
                        {'calmjs.toolchain:Toolchain': {
                            'project_name': 'calmjs',
                            'version': '1.0',
                        }}
                    ],
                    'toolchain_bin': ['artifact', '0.0.0'],
                },
                'partial.js': {
                    'builder': 'calmjs_testing_dummy:partial',
                    'toolchain_bases': [
                        {'calmjs.testing.artifact:ArtifactToolchain': {
                            'project_name': 'calmjs',
                            'version': '1.0'}},
                        {'calmjs.toolchain:NullToolchain': {
                            'project_name': 'calmjs',
                            'version': '1.0',
                        }},
                        {'calmjs.toolchain:Toolchain': {
                            'project_name': 'calmjs',
                            'version': '1.0',
                        }}
                    ],
                    'toolchain_bin': ['artifact', '0.0.0'],
                }
            },
            'versions': [
                'app 1.0',
                'calmjs 1.0',
            ]
        }, registry.get_artifact_metadata('app'))

        # test that the 'calmjs_artifacts' listing only grows - the only
        # way to clean this is to remove and rebuild egg-info directly.
        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'extra.js = calmjs_testing_dummy:extra',
            ])),
        ), 'app', '1.0', working_dir=working_dir)

        mock_ws = WorkingSet([working_dir])
        utils.stub_item_attr_value(self, dist, 'default_working_set', mock_ws)
        utils.stub_item_attr_value(
            self, artifact, 'get_bin_version_str', version)
        registry = ArtifactRegistry('calmjs.artifacts', _working_set=mock_ws)

        registry.process_package('app')
        self.assertEqual(3, len(registry.get_artifact_metadata('app')[
            'calmjs_artifacts']))
        self.assertIn('extra.js', registry.get_artifact_metadata('app')[
            'calmjs_artifacts'])

        # try again using the artifact builder
        from calmjs.registry import _inst
        _inst.records.pop('calmjs.artifacts', None)
        self.addCleanup(_inst.records.pop, 'calmjs.artifacts')
        _inst.records['calmjs.artifacts'] = registry
        builder = ArtifactBuilder('calmjs.artifacts')
        self.assertTrue(builder(['app']))


class ArtifactRegistryBuildFailureTestCase(unittest.TestCase):
    """
    Test out various build failure cases.
    """

    def setUp(self):
        # bad dummy builder
        def bad_builder():
            "Wrong function signature"

        # produces wrong output
        def malformed_builder(package_names, export_target):
            "does not produce an artifact"
            return NullToolchain()

        def blank_spec(package_names, export_target):
            "does not produce an artifact"
            return NullToolchain(), Spec()

        # nothing dummy builder
        def nothing_builder(package_names, export_target):
            "does not produce an artifact"
            return NullToolchain(), Spec(export_target=export_target)

        # inject dummy module and add cleanup
        mod = ModuleType('calmjs_testing_dummy')
        mod.bad_builder = bad_builder
        mod.nothing_builder = nothing_builder
        mod.malformed_builder = malformed_builder
        mod.blank_spec = blank_spec
        self.addCleanup(sys.modules.pop, 'calmjs_testing_dummy')
        sys.modules['calmjs_testing_dummy'] = mod

        working_dir = utils.mkdtemp(self)

        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'not_exist.js = calmjs_testing_dummy:not_exist',
                'bad.js = calmjs_testing_dummy:bad_builder',
                'nothing.js = calmjs_testing_dummy:nothing_builder',
            ])),
        ), 'app', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'bad.js = calmjs_testing_dummy:bad_builder',
                'nothing.js = calmjs_testing_dummy:nothing_builder',
            ])),
        ), 'bad', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'malformed.js = calmjs_testing_dummy:malformed_builder',
            ])),
        ), 'malformed', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'blank.js = calmjs_testing_dummy:blank_spec',
            ])),
        ), 'blank', '1.0', working_dir=working_dir)

        utils.make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'nothing.js = calmjs_testing_dummy:nothing_builder',
            ])),
        ), 'nothing', '1.0', working_dir=working_dir)

        mock_ws = WorkingSet([working_dir])
        utils.stub_item_attr_value(self, dist, 'default_working_set', mock_ws)
        self.registry = ArtifactRegistry(
            'calmjs.artifacts', _working_set=mock_ws)

    def test_build_artifacts_logs_and_failures(self):
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.registry.process_package('app')

        log = stream.getvalue()
        self.assertIn(
            "unable to import the target builder for the entry point "
            "'not_exist.js = calmjs_testing_dummy:not_exist' from package "
            "'app 1.0'", log
        )

        self.assertIn(
            "the builder referenced by the entry point "
            "'bad.js = calmjs_testing_dummy:bad_builder' from package "
            "'app 1.0' has an incompatible signature", log
        )

        # try again using the artifact builder
        from calmjs.registry import _inst
        _inst.records.pop('calmjs.artifacts', None)
        self.addCleanup(_inst.records.pop, 'calmjs.artifacts')
        _inst.records['calmjs.artifacts'] = self.registry
        builder = ArtifactBuilder('calmjs.artifacts')
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.assertFalse(builder(['app']))

        log = stream.getvalue()
        self.assertIn(
            "unable to import the target builder for the entry point "
            "'not_exist.js = calmjs_testing_dummy:not_exist' from package "
            "'app 1.0'", log
        )

    def test_existing_removed(self):
        # force an existing file
        target = self.registry.records[('app', 'nothing.js')]
        os.mkdir(dirname(target))
        with open(target, 'w'):
            pass

        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.registry.process_package('app')

        log = stream.getvalue()
        self.assertIn(
            "package 'app' has declared 3 entry points for the "
            "'calmjs.artifacts' registry for artifact construction", log
        )
        log = stream.getvalue()
        self.assertIn("removing existing export target at ", log)
        self.assertFalse(exists(target))

    def test_grandparent_not_removed(self):
        with open(dirname(self.registry.records[('bad', 'bad.js')]), 'w'):
            pass

        with self.assertRaises(ToolchainAbort):
            with pretty_logging(stream=mocks.StringIO()) as stream:
                self.registry.process_package('bad')

        log = stream.getvalue()
        self.assertIn("its dirname does not lead to a directory", log)

    def test_malformed_builder_handling(self):
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.registry.process_package('malformed')

        log = stream.getvalue()
        self.assertIn("failed to produce a valid toolchain", log)

    def test_spec_missing_export_path_handling(self):
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.registry.process_package('blank')

        log = stream.getvalue()
        self.assertIn(
            "failed to produce a spec with the expected export_target", log)

    def test_artifact_generation_failure(self):
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.registry.process_package('nothing')

        log = stream.getvalue()
        self.assertIn(
            "the entry point "
            "'nothing.js = calmjs_testing_dummy:nothing_builder' from package "
            "'nothing 1.0' failed to generate an artifact", log
        )

    def test_no_declaration(self):
        with pretty_logging(stream=mocks.StringIO()) as stream:
            self.registry.process_package('undeclared')

        log = stream.getvalue()
        self.assertIn(
            "package 'undeclared' has not declared any entry points for the "
            "'calmjs.artifacts' registry for artifact construction", log
        )

    def test_artifact_metadata_malformed(self):
        with open(self.registry.metadata.get('app'), 'w') as fd:
            fd.write('{{{invalidjson')
        self.assertEqual({}, self.registry.get_artifact_metadata('app'))