Repository: dbt-labs/dbt-core

Issue:
plugin macro name collisions result in unhelpful errors
### Describe the bug
If an adapter plugin macro and a macro defined in dbt-core have the same name, dbt gives a bad error:

```
'ParsedMacro' object has no attribute 'namespace'
```

This happens because `dbt.exceptions.raise_duplicate_macro_name` accesses `macro_1.namespace`. It should be `macro_1.name`.

### Steps To Reproduce
Create an adapter plugin with a macro that has a name the same as one in core (or another plugin). For example, `is_incremental`. Install that plugin, run dbt.

### Expected behavior
I should get an error indicating that two macros that ended up in the same namespace have the same name.

### Screenshots and log output
N/A

### System information
**Which database are you using dbt with?**
- [ ] postgres
- [ ] redshift
- [ ] bigquery
- [ ] snowflake
- [X] other (specify: any custom adapter)


**The output of `dbt --version`:**
```
0.16.0
```

**The operating system you're using:**
any

**The output of `python --version`:**
any

### Additional context
This issue is a real pain to surface and writing a real integration test for it would be a real chore, but we could at least unit test it!

I would like to get this into 0.16.1, if possible. It's a small fix and buggy error handling is always embarrassing.


Discussion:
@beckjake just added this to the 0.16.1 milestone if you think we can sneak it in!

Resolving patch:
diff --git a/core/dbt/exceptions.py b/core/dbt/exceptions.py
--- a/core/dbt/exceptions.py
+++ b/core/dbt/exceptions.py
@@ -644,10 +644,10 @@ def approximate_relation_match(target, relation):
 
 
 def raise_duplicate_macro_name(node_1, node_2, namespace) -> NoReturn:
-    duped_name = node_1.namespace
+    duped_name = node_1.name
     if node_1.package_name != node_2.package_name:
         extra = (
-            ' ({} and {} are both in the {} namespace)'
+            ' ("{}" and "{}" are both in the "{}" namespace)'
             .format(node_1.package_name, node_2.package_name, namespace)
         )
     else:


Test patch:
diff --git a/test/unit/test_exceptions.py b/test/unit/test_exceptions.py
new file mode 100644
--- /dev/null
+++ b/test/unit/test_exceptions.py
@@ -0,0 +1,35 @@
+import pytest
+
+from dbt.exceptions import raise_duplicate_macro_name, CompilationException
+from .utils import MockMacro
+
+
+def test_raise_duplicate_macros_different_package():
+    macro_1 = MockMacro(package='dbt', name='some_macro')
+    macro_2 = MockMacro(package='dbt-myadapter', name='some_macro')
+
+    with pytest.raises(CompilationException) as exc:
+        raise_duplicate_macro_name(
+            node_1=macro_1,
+            node_2=macro_2,
+            namespace='dbt',
+        )
+    assert 'dbt-myadapter' in str(exc.value)
+    assert 'some_macro' in str(exc.value)
+    assert 'namespace "dbt"' in str(exc.value)
+    assert '("dbt" and "dbt-myadapter" are both in the "dbt" namespace)' in str(exc.value)
+
+
+def test_raise_duplicate_macros_same_package():
+    macro_1 = MockMacro(package='dbt', name='some_macro')
+    macro_2 = MockMacro(package='dbt', name='some_macro')
+
+    with pytest.raises(CompilationException) as exc:
+        raise_duplicate_macro_name(
+            node_1=macro_1,
+            node_2=macro_2,
+            namespace='dbt',
+        )
+    assert 'some_macro' in str(exc.value)
+    assert 'namespace "dbt"' in str(exc.value)
+    assert "are both in" not in str(exc.value)
diff --git a/test/unit/test_graph.py b/test/unit/test_graph.py
--- a/test/unit/test_graph.py
+++ b/test/unit/test_graph.py
@@ -24,19 +24,7 @@
 
 from dbt.logger import GLOBAL_LOGGER as logger # noqa
 
-from .utils import config_from_parts_or_dicts, generate_name_macros
-
-
-def MockMacro(package, name='my_macro', kwargs={}):
-    macro = MagicMock(
-        __class__=ParsedMacro,
-        resource_type=NodeType.Macro,
-        package_name=package,
-        unique_id=f'macro.{package}.{name}',
-        **kwargs
-    )
-    macro.name = name
-    return macro
+from .utils import config_from_parts_or_dicts, generate_name_macros, MockMacro
 
 
 class GraphTest(unittest.TestCase):
diff --git a/test/unit/test_manifest.py b/test/unit/test_manifest.py
--- a/test/unit/test_manifest.py
+++ b/test/unit/test_manifest.py
@@ -16,7 +16,6 @@
     DependsOn,
     NodeConfig,
     ParsedSeedNode,
-    ParsedMacro,
     ParsedSourceDefinition,
     ParsedDocumentation,
 )
@@ -24,6 +23,8 @@
 from dbt.node_types import NodeType
 import freezegun
 
+from .utils import MockMacro
+
 
 REQUIRED_PARSED_NODE_KEYS = frozenset({
     'alias', 'tags', 'config', 'unique_id', 'refs', 'sources', 'meta',
@@ -634,19 +635,6 @@ def test__build_flat_graph(self):
 
 
 # Tests of the manifest search code (find_X_by_Y)
-
-def MockMacro(package, name='my_macro', kwargs={}):
-    macro = mock.MagicMock(
-        __class__=ParsedMacro,
-        resource_type=NodeType.Macro,
-        package_name=package,
-        unique_id=f'macro.{package}.{name}',
-        **kwargs
-    )
-    macro.name = name
-    return macro
-
-
 def MockMaterialization(package, name='my_materialization', adapter_type=None, kwargs={}):
     if adapter_type is None:
         adapter_type = 'default'
diff --git a/test/unit/utils.py b/test/unit/utils.py
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -187,3 +187,24 @@ def _make_table_of(self, rows, column_types):
             column_types = [self._get_tester_for(typ) for typ in column_types]
         table = agate.Table(rows, column_names=column_names, column_types=column_types)
         return table
+
+
+def MockMacro(package, name='my_macro', kwargs={}):
+    from dbt.contracts.graph.parsed import ParsedMacro
+    from dbt.node_types import NodeType
+
+    mock_kwargs = dict(
+        resource_type=NodeType.Macro,
+        package_name=package,
+        unique_id=f'macro.{package}.{name}',
+        original_file_path='/dev/null',
+    )
+
+    mock_kwargs.update(kwargs)
+
+    macro = mock.MagicMock(
+        spec=ParsedMacro,
+        **mock_kwargs
+    )
+    macro.name = name
+    return macro