Repository: facebookresearch/hydra

Issue:
[Bug] Bad config group choice with a comma in normal run does not result in an error
```
$ python examples/tutorials/basic/your_first_hydra_app/5_defaults/my_app.py  db=mysql,a
{}
```

Discussion:
lgtm-com[bot]: This pull request **introduces 1 alert** when merging cce3a4f3d483510d0766b03031547d04d043d009 into b09c5b94315904cde52f3fa92d70a0393f81afc0 - [view on LGTM.com](https://lgtm.com/projects/g/facebookresearch/hydra/rev/pr-a86c54ae6fc87012e1395f3d84fb0df6ebb73911)

**new alerts:**

* 1 for Unnecessary pass

Patch:
diff --git a/hydra/_internal/config_loader_impl.py b/hydra/_internal/config_loader_impl.py
index ab9b42573e..d936cff619 100644
--- a/hydra/_internal/config_loader_impl.py
+++ b/hydra/_internal/config_loader_impl.py
@@ -25,6 +25,12 @@ from hydra.core.object_type import ObjectType
 from hydra.core.utils import JobRuntime
 from hydra.errors import HydraException, MissingConfigException
 from hydra.plugins.config_source import ConfigLoadError, ConfigSource
+from hydra.types import RunMode
+
+
+class UnspecifiedMandatoryDefault(Exception):
+    def __init__(self, config_group: str,) -> None:
+        self.config_group = config_group
 
 
 @dataclass
@@ -77,6 +83,9 @@ class ParsedOverrideWithLine:
     override: ParsedOverride
     input_line: str
 
+    def __repr__(self) -> str:
+        return f"[Parsed] {self.input_line}"
+
 
 @dataclass
 class DefaultElement:
@@ -178,12 +187,28 @@ class ConfigLoaderImpl(ConfigLoader):
         self,
         config_name: Optional[str],
         overrides: List[str],
+        run_mode: RunMode,
         strict: Optional[bool] = None,
     ) -> DictConfig:
         if strict is None:
             strict = self.default_strict
 
         parsed_overrides = [self._parse_override(override) for override in overrides]
+        filtered_parsed_overrides = []
+        for x in parsed_overrides:
+            if ConfigLoaderImpl._is_sweeper_override(x.override):
+                if run_mode == RunMode.RUN:
+                    raise HydraException(
+                        f"'{x.input_line}' is a sweeper override, did you forget to to use --multirun|-m ?"
+                    )
+                elif run_mode == RunMode.MULTIRUN:
+                    # filter, the sweeper will pass pass concrete overrides to individual jobs
+                    if x.override.key.startswith("hydra."):
+                        raise HydraException(
+                            f"Sweeping over Hydra configuration is not supported : '{x.input_line}'"
+                        )
+            else:
+                filtered_parsed_overrides.append(x)
 
         if config_name is not None and not self.repository.config_exists(config_name):
             self.missing_config_error(
@@ -219,15 +244,32 @@ class ConfigLoaderImpl(ConfigLoader):
         split_at = len(defaults)
 
         config_group_overrides, config_overrides = self.split_overrides(
-            parsed_overrides
+            filtered_parsed_overrides
         )
+
         self._combine_default_lists(defaults, job_defaults)
         ConfigLoaderImpl._apply_overrides_to_defaults(config_group_overrides, defaults)
 
         # Load and defaults and merge them into cfg
-        cfg = self._merge_defaults_into_config(
-            hydra_cfg, job_cfg, job_cfg_load_trace, defaults, split_at
-        )
+        try:
+            cfg = self._merge_defaults_into_config(
+                hydra_cfg,
+                job_cfg,
+                job_cfg_load_trace,
+                defaults,
+                split_at,
+                run_mode=run_mode,
+            )
+        except UnspecifiedMandatoryDefault as e:
+            options = self.get_group_options(e.config_group)
+            opt_list = "\n".join(["\t" + x for x in options])
+            msg = (
+                f"You must specify '{e.config_group}', e.g, {e.config_group}=<OPTION>"
+                f"\nAvailable options:"
+                f"\n{opt_list}"
+            )
+            raise HydraException(msg) from e
+
         OmegaConf.set_struct(cfg.hydra, True)
         OmegaConf.set_struct(cfg, strict)
 
@@ -274,6 +316,7 @@ class ConfigLoaderImpl(ConfigLoader):
             config_name=master_config.hydra.job.config_name,
             strict=self.default_strict,
             overrides=overrides,
+            run_mode=RunMode.RUN,
         )
 
         with open_dict(sweep_config):
@@ -317,6 +360,11 @@ class ConfigLoaderImpl(ConfigLoader):
                 matches.append(default)
         return matches
 
+    # TODO: should be a function in the sweeper. somehow.
+    @staticmethod
+    def _is_sweeper_override(override: ParsedOverride) -> bool:
+        return override.value is not None and "," in override.value
+
     @staticmethod
     def _apply_overrides_to_defaults(
         overrides: List[ParsedOverrideWithLine], defaults: List[DefaultElement],
@@ -355,11 +403,6 @@ class ConfigLoaderImpl(ConfigLoader):
                     "Add syntax does not support package rename, remove + prefix"
                 )
 
-            if override.value is not None and "," in override.value:
-                # If this is a multirun config (comma separated list), flag the default to prevent it from being
-                # loaded until we are constructing the config for individual jobs.
-                override.value = "_SKIP_"
-
             matches = ConfigLoaderImpl.find_matches(key_to_defaults, override)
 
             if override.is_delete():
@@ -696,8 +739,11 @@ class ConfigLoaderImpl(ConfigLoader):
                     else:
                         options = self.get_group_options(config_group)
                         if options:
-                            lst = "\n\t".join(options)
-                            msg = f"Could not load {new_cfg}, available options:\n{config_group}:\n\t{lst}"
+                            opt_list = "\n".join(["\t" + x for x in options])
+                            msg = (
+                                f"Could not load {new_cfg}.\nAvailable options:"
+                                f"\n{opt_list}"
+                            )
                         else:
                             msg = f"Could not load {new_cfg}"
                         raise MissingConfigException(msg, new_cfg, options)
@@ -718,6 +764,7 @@ class ConfigLoaderImpl(ConfigLoader):
         job_cfg_load_trace: Optional[LoadTrace],
         defaults: List[DefaultElement],
         split_at: int,
+        run_mode: RunMode,
     ) -> DictConfig:
         def merge_defaults_list_into_config(
             merged_cfg: DictConfig, def_list: List[DefaultElement]
@@ -734,7 +781,19 @@ class ConfigLoaderImpl(ConfigLoader):
 
             for idx, default1 in enumerate(def_list):
                 if default1.config_group is not None:
-                    config_name = dict_with_list.defaults[idx][default1.config_group]
+                    if OmegaConf.is_missing(
+                        dict_with_list.defaults[idx], default1.config_group
+                    ):
+                        if run_mode == RunMode.RUN:
+                            raise UnspecifiedMandatoryDefault(
+                                config_group=default1.config_group
+                            )
+                        else:
+                            config_name = "???"
+                    else:
+                        config_name = dict_with_list.defaults[idx][
+                            default1.config_group
+                        ]
                 else:
                     config_name = dict_with_list.defaults[idx]
 
@@ -746,7 +805,7 @@ class ConfigLoaderImpl(ConfigLoader):
                     if job_cfg_load_trace is not None:
                         self.all_config_checked.append(job_cfg_load_trace)
                 elif default1.config_group is not None:
-                    if default1.config_name not in (None, "_SKIP_"):
+                    if default1.config_name not in (None, "_SKIP_", "???"):
                         merged_cfg = self._merge_config(
                             cfg=merged_cfg,
                             config_group=default1.config_group,
diff --git a/hydra/_internal/hydra.py b/hydra/_internal/hydra.py
index 320cc96df9..31aa423e8a 100644
--- a/hydra/_internal/hydra.py
+++ b/hydra/_internal/hydra.py
@@ -29,7 +29,7 @@ from hydra.plugins.config_source import ConfigSource
 from hydra.plugins.launcher import Launcher
 from hydra.plugins.search_path_plugin import SearchPathPlugin
 from hydra.plugins.sweeper import Sweeper
-from hydra.types import TaskFunction
+from hydra.types import RunMode, TaskFunction
 
 from .config_loader_impl import ConfigLoaderImpl
 from .utils import create_automatic_config_search_path
@@ -96,7 +96,10 @@ class Hydra:
         overrides: List[str],
     ) -> JobReturn:
         cfg = self.compose_config(
-            config_name=config_name, overrides=overrides, with_log_configuration=True
+            config_name=config_name,
+            overrides=overrides,
+            with_log_configuration=True,
+            run_mode=RunMode.RUN,
         )
         HydraConfig.instance().set_config(cfg)
         return run_job(
@@ -118,6 +121,7 @@ class Hydra:
             overrides=overrides,
             strict=False,
             with_log_configuration=True,
+            run_mode=RunMode.MULTIRUN,
         )
         HydraConfig.instance().set_config(cfg)
         sweeper = Plugins.instance().instantiate_sweeper(
@@ -149,6 +153,7 @@ class Hydra:
         cfg = self.compose_config(
             config_name=config_name,
             overrides=overrides,
+            run_mode=RunMode.RUN,
             with_log_configuration=with_log_configuration,
         )
         if cfg_type == "job":
@@ -304,6 +309,7 @@ class Hydra:
         cfg = self.compose_config(
             config_name=config_name,
             overrides=args.overrides,
+            run_mode=RunMode.RUN,
             with_log_configuration=True,
         )
         help_cfg = cfg.hydra.hydra_help
@@ -317,6 +323,7 @@ class Hydra:
         cfg = self.compose_config(
             config_name=config_name,
             overrides=args.overrides,
+            run_mode=RunMode.RUN,
             with_log_configuration=True,
         )
         help_cfg = cfg.hydra.help
@@ -478,13 +485,14 @@ class Hydra:
         self,
         config_name: Optional[str],
         overrides: List[str],
+        run_mode: RunMode,
         strict: Optional[bool] = None,
         with_log_configuration: bool = False,
     ) -> DictConfig:
         """
-        :param self:
         :param config_name:
         :param overrides:
+        :param run_mode: compose config for run or for multirun?
         :param with_log_configuration: True to configure logging subsystem from the loaded config
         :param strict: None for default behavior (default to true for config file, false if no config file).
                        otherwise forces specific behavior.
@@ -494,7 +502,10 @@ class Hydra:
         self.config_loader.ensure_main_config_source_available()
 
         cfg = self.config_loader.load_configuration(
-            config_name=config_name, overrides=overrides, strict=strict
+            config_name=config_name,
+            overrides=overrides,
+            strict=strict,
+            run_mode=run_mode,
         )
         with open_dict(cfg):
             from hydra import __version__
diff --git a/hydra/_internal/utils.py b/hydra/_internal/utils.py
index 2865e1f9a2..8254bcb178 100644
--- a/hydra/_internal/utils.py
+++ b/hydra/_internal/utils.py
@@ -196,7 +196,7 @@ def run_and_report(func: Any) -> Any:
         else:
             sys.stderr.write(str(ex) + "\n")
             if ex.__cause__ is not None:
-                sys.stderr.write(str(ex.__cause__) + "\n")
+                sys.stderr.write(str(ex.__cause__))
 
             sys.stderr.write(
                 "\nSet the environment variable HYDRA_FULL_ERROR=1 for a complete stack trace\n"
diff --git a/hydra/core/config_loader.py b/hydra/core/config_loader.py
index 38c9148216..dae9c25daa 100644
--- a/hydra/core/config_loader.py
+++ b/hydra/core/config_loader.py
@@ -8,6 +8,7 @@ from omegaconf import DictConfig
 from hydra.core.config_search_path import ConfigSearchPath
 from hydra.core.object_type import ObjectType
 from hydra.plugins.config_source import ConfigSource
+from hydra.types import RunMode
 
 
 @dataclass
@@ -28,6 +29,7 @@ class ConfigLoader(ABC):
         self,
         config_name: Optional[str],
         overrides: List[str],
+        run_mode: RunMode,
         strict: Optional[bool] = None,
     ) -> DictConfig:
         ...
diff --git a/hydra/experimental/compose.py b/hydra/experimental/compose.py
index f2e077b61a..d6354e493a 100644
--- a/hydra/experimental/compose.py
+++ b/hydra/experimental/compose.py
@@ -4,6 +4,7 @@ from typing import List, Optional
 from omegaconf import DictConfig, open_dict
 
 from hydra.core.global_hydra import GlobalHydra
+from hydra.types import RunMode
 
 
 def compose(
@@ -28,7 +29,10 @@ def compose(
     gh = GlobalHydra.instance()
     assert gh.hydra is not None
     cfg = gh.hydra.compose_config(
-        config_name=config_name, overrides=overrides, strict=strict
+        config_name=config_name,
+        overrides=overrides,
+        run_mode=RunMode.RUN,
+        strict=strict,
     )
     assert isinstance(cfg, DictConfig)
 
diff --git a/hydra/plugins/completion_plugin.py b/hydra/plugins/completion_plugin.py
index e8523e9183..30f011382b 100644
--- a/hydra/plugins/completion_plugin.py
+++ b/hydra/plugins/completion_plugin.py
@@ -22,6 +22,7 @@ from omegaconf import (
 from hydra.core.config_loader import ConfigLoader
 from hydra.core.object_type import ObjectType
 from hydra.plugins.plugin import Plugin
+from hydra.types import RunMode
 
 
 class CompletionPlugin(Plugin):
@@ -211,7 +212,7 @@ class CompletionPlugin(Plugin):
             words = words[0:-1]
 
         config = self.config_loader.load_configuration(
-            config_name=config_name, overrides=words, strict=True
+            config_name=config_name, overrides=words, run_mode=RunMode.RUN, strict=True
         )
 
         fname_prefix, filename = CompletionPlugin._get_filename(word)
diff --git a/hydra/types.py b/hydra/types.py
index 7b0182a82e..b1a99ae46c 100644
--- a/hydra/types.py
+++ b/hydra/types.py
@@ -1,5 +1,6 @@
 # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
 from dataclasses import dataclass, field
+from enum import Enum
 from typing import Any, Callable, Dict
 
 from omegaconf import MISSING
@@ -23,3 +24,8 @@ class ObjectConf(Dict[str, Any]):
     # class is deprecated, use target, class will be removed in Hydra 1.1
     # (class is Python keyword and is only supported through DictConfig)
     # class: str = MISSING
+
+
+class RunMode(Enum):
+    RUN = 1
+    MULTIRUN = 2
diff --git a/news/712.bugfix b/news/712.bugfix
new file mode 100644
index 0000000000..045ca1fda2
--- /dev/null
+++ b/news/712.bugfix
@@ -0,0 +1,1 @@
+Prints error when command line override looks like a sweep but --multirun is not specified
diff --git a/news/726.bugfix b/news/726.bugfix
new file mode 100644
index 0000000000..5809670275
--- /dev/null
+++ b/news/726.bugfix
@@ -0,0 +1,1 @@
+Fixed handling of multirun when target config node is introduced by composition


Test patch:
diff --git a/hydra/test_utils/launcher_common_tests.py b/hydra/test_utils/launcher_common_tests.py
index 9275080a2b..c49ac46c10 100644
--- a/hydra/test_utils/launcher_common_tests.py
+++ b/hydra/test_utils/launcher_common_tests.py
@@ -3,6 +3,7 @@
 Common test functions testing launchers
 """
 import copy
+import re
 from pathlib import Path
 from typing import Any, List, Optional, Set
 
@@ -10,6 +11,7 @@ import pytest
 from omegaconf import DictConfig, OmegaConf
 
 from hydra import TaskFunction
+from hydra.errors import HydraException
 from hydra.test_utils.test_utils import (
     TSweepRunner,
     integration_test,
@@ -47,11 +49,26 @@ class LauncherTestSuite:
     def test_not_sweeping_hydra_overrides(
         self, hydra_sweep_runner: TSweepRunner, launcher_name: str, overrides: List[str]
     ) -> None:
-        not_sweeping_hydra_overrides(
-            hydra_sweep_runner,
-            overrides=["hydra/launcher=" + launcher_name] + overrides,
-            task_function=self.task_function,
-        )
+        with pytest.raises(
+            HydraException,
+            match=re.escape(
+                "Sweeping over Hydra configuration is not supported : 'hydra.verbose=true,false'"
+            ),
+        ):
+            with hydra_sweep_runner(
+                calling_file=None,
+                calling_module="hydra.test_utils.a_module",
+                task_function=None,
+                config_path="configs",
+                config_name="compose.yaml",
+                overrides=overrides
+                + [
+                    "hydra/launcher=" + launcher_name,
+                    "+a=0,1",
+                    "hydra.verbose=true,false",
+                ],
+            ):
+                pass
 
     def test_sweep_1_job_strict(
         self, hydra_sweep_runner: TSweepRunner, launcher_name: str, overrides: List[str]
@@ -294,36 +311,6 @@ def sweep_2_jobs(
             assert path.exists(), f"'{path}' does not exist, dirs: {lst}"
 
 
-def not_sweeping_hydra_overrides(
-    hydra_sweep_runner: TSweepRunner,
-    overrides: List[str],
-    task_function: Optional[TaskFunction],
-) -> None:
-    """
-    Runs a sweep with two jobs
-    """
-    overrides.extend(["+a=0,1", "hydra.verbose=true,false"])
-    sweep = hydra_sweep_runner(
-        calling_file=None,
-        calling_module="hydra.test_utils.a_module",
-        task_function=task_function,
-        config_path="configs",
-        config_name="compose.yaml",
-        overrides=overrides,
-    )
-    base = OmegaConf.create({"foo": 10, "bar": 100})
-
-    with sweep:
-        assert sweep.returns is not None
-        assert len(sweep.returns[0]) == 2
-        for i in range(2):
-            job_ret = sweep.returns[0][i]
-            expected_conf = OmegaConf.merge(base, {"a": i})
-            assert job_ret.overrides == [f"+a={i}"]
-            assert job_ret.cfg == expected_conf
-            verify_dir_outputs(job_ret, job_ret.overrides)
-
-
 def sweep_two_config_groups(
     hydra_sweep_runner: TSweepRunner,
     overrides: List[str],
diff --git a/tests/__init__.py b/tests/__init__.py
index bdcf175878..b2320658c6 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -3,6 +3,15 @@ from dataclasses import dataclass
 from typing import Any, List, Tuple
 
 
+def normalize_newlines(s: str) -> str:
+    """
+    Normalizes new lines such they are comparable across different operating systems
+    :param s:
+    :return:
+    """
+    return s.replace("\r\n", "\n").replace("\r", "\n")
+
+
 def verify_hydra_pytest_plugin_not_installed() -> None:
     try:
         # This check can be removed in 1.1
diff --git a/tests/test_apps/multirun_structured_conflict/config.yaml b/tests/test_apps/multirun_structured_conflict/config.yaml
new file mode 100644
index 0000000000..b1278dcc74
--- /dev/null
+++ b/tests/test_apps/multirun_structured_conflict/config.yaml
@@ -0,0 +1,2 @@
+defaults:
+  - test: default
diff --git a/tests/test_apps/multirun_structured_conflict/my_app.py b/tests/test_apps/multirun_structured_conflict/my_app.py
new file mode 100644
index 0000000000..9bc4fce0b6
--- /dev/null
+++ b/tests/test_apps/multirun_structured_conflict/my_app.py
@@ -0,0 +1,25 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
+from dataclasses import dataclass
+
+from omegaconf import MISSING, DictConfig
+
+import hydra
+from hydra.core.config_store import ConfigStore
+
+
+@dataclass
+class TestConfig:
+    param: int = MISSING
+
+
+config_store = ConfigStore.instance()
+config_store.store(group="test", name="default", node=TestConfig)
+
+
+@hydra.main(config_name="config")
+def run(config: DictConfig):
+    print(config.test.param)
+
+
+if __name__ == "__main__":
+    run()
diff --git a/tests/test_apps/simple_app/__init__.py b/tests/test_apps/simple_app/__init__.py
new file mode 100644
index 0000000000..168f9979a4
--- /dev/null
+++ b/tests/test_apps/simple_app/__init__.py
@@ -0,0 +1,1 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
diff --git a/tests/test_apps/simple_app/my_app.py b/tests/test_apps/simple_app/my_app.py
new file mode 100644
index 0000000000..d4628cedf2
--- /dev/null
+++ b/tests/test_apps/simple_app/my_app.py
@@ -0,0 +1,13 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
+from omegaconf import DictConfig
+
+import hydra
+
+
+@hydra.main()
+def my_app(cfg: DictConfig) -> None:
+    print(cfg.pretty())
+
+
+if __name__ == "__main__":
+    my_app()
diff --git a/tests/test_compose.py b/tests/test_compose.py
index 40c210d476..ea29c48d15 100644
--- a/tests/test_compose.py
+++ b/tests/test_compose.py
@@ -270,7 +270,7 @@ def test_missing_init_py_error(hydra_restore_singletons: Any) -> None:
         ):
             hydra = GlobalHydra.instance().hydra
             assert hydra is not None
-            hydra.compose_config(config_name="test.yaml", overrides=[])
+            compose(config_name="test.yaml", overrides=[])
 
 
 def test_missing_bad_config_dir_error(hydra_restore_singletons: Any) -> None:
@@ -283,7 +283,7 @@ def test_missing_bad_config_dir_error(hydra_restore_singletons: Any) -> None:
         with initialize_config_dir(config_dir="/no_way_in_hell_1234567890"):
             hydra = GlobalHydra.instance().hydra
             assert hydra is not None
-            hydra.compose_config(config_name="test.yaml", overrides=[])
+            compose(config_name="test.yaml", overrides=[])
 
 
 def test_initialize_with_module(hydra_restore_singletons: Any) -> None:
diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py
index 6eaa95d5ef..5f16fb325a 100644
--- a/tests/test_config_loader.py
+++ b/tests/test_config_loader.py
@@ -18,6 +18,7 @@ from hydra.core.config_store import ConfigStore, ConfigStoreWithProvider
 from hydra.core.utils import env_override
 from hydra.errors import HydraException, MissingConfigException
 from hydra.test_utils.test_utils import chdir_hydra_root
+from hydra.types import RunMode
 
 chdir_hydra_root()
 
@@ -62,7 +63,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="config.yaml", strict=False, overrides=["abc=123"]
+            config_name="config.yaml",
+            strict=False,
+            overrides=["abc=123"],
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -74,7 +78,10 @@ class TestConfigLoader:
         )
         with pytest.raises(MissingConfigException):
             config_loader.load_configuration(
-                config_name="missing-default.yaml", overrides=[], strict=False
+                config_name="missing-default.yaml",
+                overrides=[],
+                strict=False,
+                run_mode=RunMode.RUN,
             )
 
     def test_load_with_missing_optional_default(self, path: str) -> None:
@@ -82,7 +89,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="missing-optional-default.yaml", overrides=[], strict=False
+            config_name="missing-optional-default.yaml",
+            overrides=[],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -93,7 +103,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="optional-default.yaml", overrides=[], strict=False
+            config_name="optional-default.yaml",
+            overrides=[],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -107,6 +120,7 @@ class TestConfigLoader:
             config_name="optional-default.yaml",
             overrides=["group1=file2"],
             strict=False,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -139,7 +153,7 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(f"{path}/package_tests")
         )
         cfg = config_loader.load_configuration(
-            config_name="pkg_override", overrides=overrides
+            config_name="pkg_override", overrides=overrides, run_mode=RunMode.RUN
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -186,7 +200,9 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(f"{path}/package_tests")
         )
         cfg = config_loader.load_configuration(
-            config_name="two_packages_one_group", overrides=overrides
+            config_name="two_packages_one_group",
+            overrides=overrides,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -200,6 +216,7 @@ class TestConfigLoader:
             config_name="optional-default.yaml",
             overrides=["+group2=file1"],
             strict=False,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -213,6 +230,7 @@ class TestConfigLoader:
             config_name="overriding_run_dir.yaml",
             overrides=["hydra.run.dir=abc"],
             strict=False,
+            run_mode=RunMode.RUN,
         )
         assert cfg.hydra.run.dir == "abc"
 
@@ -221,7 +239,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="overriding_run_dir.yaml", overrides=[], strict=False
+            config_name="overriding_run_dir.yaml",
+            overrides=[],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
         assert cfg.hydra.run.dir == "cde"
 
@@ -235,7 +256,10 @@ class TestConfigLoader:
         )
         # Test that overriding existing things works in strict mode
         cfg = config_loader.load_configuration(
-            config_name="compose.yaml", overrides=["foo=ZZZ"], strict=True
+            config_name="compose.yaml",
+            overrides=["foo=ZZZ"],
+            strict=True,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -249,7 +273,10 @@ class TestConfigLoader:
         # Test that bad overrides triggers the KeyError
         with pytest.raises(HydraException):
             config_loader.load_configuration(
-                config_name="compose.yaml", overrides=["f00=ZZZ"], strict=True
+                config_name="compose.yaml",
+                overrides=["f00=ZZZ"],
+                strict=True,
+                run_mode=RunMode.RUN,
             )
 
     def test_load_history(self, path: str) -> None:
@@ -257,7 +284,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         config_loader.load_configuration(
-            config_name="missing-optional-default.yaml", overrides=[], strict=False
+            config_name="missing-optional-default.yaml",
+            overrides=[],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
         expected = hydra_load_list.copy()
         expected.append(LoadTrace("missing-optional-default.yaml", path, "main", None))
@@ -273,6 +303,7 @@ class TestConfigLoader:
             config_name="custom_default_launcher.yaml",
             overrides=["hydra/launcher=basic"],
             strict=False,
+            run_mode=RunMode.RUN,
         )
 
         expected = hydra_load_list.copy()
@@ -284,7 +315,7 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="config.yml", overrides=[], strict=False
+            config_name="config.yml", overrides=[], strict=False, run_mode=RunMode.RUN
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -296,7 +327,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="config.yaml", overrides=["abc='cde=12'"], strict=False
+            config_name="config.yaml",
+            overrides=["abc='cde=12'"],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -307,7 +341,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="compose.yaml", overrides=["group1=abc.cde"], strict=False
+            config_name="compose.yaml",
+            overrides=["group1=abc.cde"],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -329,7 +366,7 @@ class TestConfigLoader:
         )
 
         cfg = config_loader.load_configuration(
-            config_name="config", overrides=["+db=mysql"]
+            config_name="config", overrides=["+db=mysql"], run_mode=RunMode.RUN
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -356,7 +393,7 @@ class TestConfigLoader:
         # verify illegal override is rejected during load
         with pytest.raises(HydraException):
             config_loader.load_configuration(
-                config_name="db/mysql", overrides=["db.port=fail"]
+                config_name="db/mysql", overrides=["db.port=fail"], run_mode=RunMode.RUN
             )
 
     def test_load_config_file_with_schema_validation(
@@ -371,7 +408,10 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="config", overrides=["+db=mysql"], strict=False
+            config_name="config",
+            overrides=["+db=mysql"],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
 
         with open_dict(cfg):
@@ -399,7 +439,7 @@ class TestConfigLoader:
             config_search_path=create_config_search_path(path)
         )
         cfg = config_loader.load_configuration(
-            config_name="config.yaml", overrides=["abc=null"]
+            config_name="config.yaml", overrides=["abc=null"], run_mode=RunMode.RUN
         )
         with open_dict(cfg):
             del cfg["hydra"]
@@ -472,7 +512,7 @@ def test_default_removal(config_file: str, overrides: List[str]) -> None:
         config_search_path=create_config_search_path("hydra/test_utils/configs")
     )
     config_loader.load_configuration(
-        config_name=config_file, overrides=overrides, strict=False
+        config_name=config_file, overrides=overrides, strict=False, run_mode=RunMode.RUN
     )
 
     expected = list(
@@ -490,7 +530,10 @@ def test_defaults_not_list_exception() -> None:
     )
     with pytest.raises(ValueError):
         config_loader.load_configuration(
-            config_name="defaults_not_list.yaml", overrides=[], strict=False
+            config_name="defaults_not_list.yaml",
+            overrides=[],
+            strict=False,
+            run_mode=RunMode.RUN,
         )
 
 
@@ -500,7 +543,10 @@ def test_override_hydra_config_value_from_config_file() -> None:
     )
 
     cfg = config_loader.load_configuration(
-        config_name="overriding_output_dir.yaml", overrides=[], strict=False
+        config_name="overriding_output_dir.yaml",
+        overrides=[],
+        strict=False,
+        run_mode=RunMode.RUN,
     )
     assert cfg.hydra.run.dir == "foo"
 
@@ -511,7 +557,10 @@ def test_override_hydra_config_group_from_config_file() -> None:
     )
 
     config_loader.load_configuration(
-        config_name="overriding_logging_default.yaml", overrides=[], strict=False
+        config_name="overriding_logging_default.yaml",
+        overrides=[],
+        strict=False,
+        run_mode=RunMode.RUN,
     )
 
     # This load history is too different to easily reuse the standard hydra_load_list
@@ -563,7 +612,10 @@ def test_non_config_group_default() -> None:
         config_search_path=create_config_search_path("hydra/test_utils/configs")
     )
     config_loader.load_configuration(
-        config_name="non_config_group_default.yaml", overrides=[], strict=False
+        config_name="non_config_group_default.yaml",
+        overrides=[],
+        strict=False,
+        run_mode=RunMode.RUN,
     )
 
     expected = hydra_load_list.copy()
@@ -590,7 +642,10 @@ def test_mixed_composition_order() -> None:
         config_search_path=create_config_search_path("hydra/test_utils/configs")
     )
     config_loader.load_configuration(
-        config_name="mixed_compose.yaml", overrides=[], strict=False
+        config_name="mixed_compose.yaml",
+        overrides=[],
+        strict=False,
+        run_mode=RunMode.RUN,
     )
 
     expected = hydra_load_list.copy()
@@ -621,7 +676,9 @@ def test_load_schema_as_config(hydra_restore_singletons: Any) -> None:
     )
 
     config_loader = ConfigLoaderImpl(config_search_path=create_config_search_path(None))
-    cfg = config_loader.load_configuration(config_name="config", overrides=[])
+    cfg = config_loader.load_configuration(
+        config_name="config", overrides=[], run_mode=RunMode.RUN
+    )
     with open_dict(cfg):
         del cfg["hydra"]
     assert cfg == {
@@ -675,7 +732,9 @@ def test_overlapping_schemas(hydra_restore_singletons: Any) -> None:
     cs.store(group="plugin", name="concrete", node=ConcretePlugin)
 
     config_loader = ConfigLoaderImpl(config_search_path=create_config_search_path(None))
-    cfg = config_loader.load_configuration(config_name="config", overrides=[])
+    cfg = config_loader.load_configuration(
+        config_name="config", overrides=[], run_mode=RunMode.RUN
+    )
     with open_dict(cfg):
         del cfg["hydra"]
 
@@ -683,7 +742,7 @@ def test_overlapping_schemas(hydra_restore_singletons: Any) -> None:
     assert OmegaConf.get_type(cfg.plugin) == Plugin
 
     cfg = config_loader.load_configuration(
-        config_name="config", overrides=["+plugin=concrete"]
+        config_name="config", overrides=["+plugin=concrete"], run_mode=RunMode.RUN
     )
     with open_dict(cfg):
         del cfg["hydra"]
@@ -702,14 +761,18 @@ def test_invalid_plugin_merge(hydra_restore_singletons: Any) -> Any:
 
     cl = ConfigLoaderImpl(config_search_path=create_config_search_path(None))
     with pytest.raises(HydraException):
-        cl.load_configuration(config_name="config", overrides=["plugin=invalid"])
+        cl.load_configuration(
+            config_name="config", overrides=["plugin=invalid"], run_mode=RunMode.RUN
+        )
 
 
 def test_job_env_copy() -> None:
     config_loader = ConfigLoaderImpl(config_search_path=create_config_search_path(None))
     with env_override({"zonk": "123456"}):
         cfg = config_loader.load_configuration(
-            config_name=None, overrides=["hydra.job.env_copy=[zonk]"]
+            config_name=None,
+            overrides=["hydra.job.env_copy=[zonk]"],
+            run_mode=RunMode.RUN,
         )
         assert cfg.hydra.job.env_set == {"zonk": "123456"}
 
@@ -742,7 +805,9 @@ def test_complex_defaults(overrides: Any, expected: Any) -> None:
         )
     )
 
-    cfg = config_loader.load_configuration(config_name="config", overrides=overrides)
+    cfg = config_loader.load_configuration(
+        config_name="config", overrides=overrides, run_mode=RunMode.RUN
+    )
     with open_dict(cfg):
         del cfg["hydra"]
     assert cfg == expected
diff --git a/tests/test_core_utils.py b/tests/test_core_utils.py
index 4baca6d654..a27d747229 100644
--- a/tests/test_core_utils.py
+++ b/tests/test_core_utils.py
@@ -7,6 +7,7 @@ from hydra._internal.config_loader_impl import ConfigLoaderImpl
 from hydra._internal.utils import create_config_search_path
 from hydra.core import utils
 from hydra.core.hydra_config import HydraConfig
+from hydra.types import RunMode
 
 
 def test_accessing_hydra_config(hydra_restore_singletons: Any) -> Any:
@@ -16,7 +17,7 @@ def test_accessing_hydra_config(hydra_restore_singletons: Any) -> Any:
         config_search_path=create_config_search_path("pkg://hydra.test_utils.configs")
     )
     cfg = config_loader.load_configuration(
-        config_name="accessing_hydra_config", overrides=[]
+        config_name="accessing_hydra_config", run_mode=RunMode.RUN, overrides=[]
     )
     HydraConfig.instance().set_config(cfg)
     with open_dict(cfg):
diff --git a/tests/test_hydra.py b/tests/test_hydra.py
index 84f95a4eb4..269633ba94 100644
--- a/tests/test_hydra.py
+++ b/tests/test_hydra.py
@@ -17,6 +17,7 @@ from hydra.test_utils.test_utils import (
     integration_test,
     verify_dir_outputs,
 )
+from tests import normalize_newlines
 from tests.test_examples import run_with_error
 
 chdir_hydra_root()
@@ -149,7 +150,6 @@ def test_app_with_config_file__no_overrides(
     calling_file: str,
     calling_module: str,
 ) -> None:
-
     task = hydra_task_runner(
         calling_file=calling_file,
         calling_module=calling_module,
@@ -426,7 +426,17 @@ def test_cfg(tmpdir: Path, flag: str, expected_keys: List[str]) -> None:
 @pytest.mark.parametrize(  # type: ignore
     "flags,expected",
     [
-        (
+        pytest.param(
+            ["--cfg=job"],
+            """# @package _global_
+db:
+  driver: mysql
+  user: omry
+  pass: secret
+""",
+            id="no-package",
+        ),
+        pytest.param(
             ["--cfg=job", "--package=_global_"],
             """# @package _global_
 db:
@@ -434,16 +444,20 @@ db:
   user: omry
   pass: secret
 """,
+            id="package=_global_",
         ),
-        (
+        pytest.param(
             ["--cfg=job", "--package=db"],
             """# @package db
 driver: mysql
 user: omry
 pass: secret
 """,
+            id="package=db",
+        ),
+        pytest.param(
+            ["--cfg=job", "--package=db.driver"], "mysql\n", id="package=db.driver"
         ),
-        (["--cfg=job", "--package=db.driver"], "mysql\n"),
     ],
 )
 def test_cfg_with_package(tmpdir: Path, flags: List[str], expected: str) -> None:
@@ -453,11 +467,8 @@ def test_cfg_with_package(tmpdir: Path, flags: List[str], expected: str) -> None
         "hydra.run.dir=" + str(tmpdir),
     ] + flags
 
-    def norm(s: str) -> str:
-        return s.replace("\r\n", "\n").replace("\r", "\n")
-
     result = subprocess.check_output(cmd).decode("utf-8")
-    assert norm(result) == norm(expected)
+    assert normalize_newlines(result) == expected
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -735,7 +746,12 @@ def test_override_with_invalid_group_choice(
     calling_module: str,
     override: str,
 ) -> None:
-    msg = f"""Could not load db/{override}, available options:\ndb:\n\tmysql\n\tpostgresql"""
+    msg = (
+        f"Could not load db/{override}."
+        f"\nAvailable options:"
+        f"\n\tmysql"
+        f"\n\tpostgresql"
+    )
 
     with pytest.raises(MissingConfigException, match=re.escape(msg)):
         with hydra_task_runner(
@@ -829,3 +845,58 @@ def test_module_run(
     else:
         result = subprocess.check_output(cmd, env=modified_env)
         assert OmegaConf.create(str(result.decode("utf-8"))) == {"x": 10}
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "overrides,error,expected",
+    [
+        (["test.param=1"], False, "1\n"),
+        (
+            ["test.param=1,2"],
+            True,
+            "'test.param=1,2' is a sweeper override, did you forget to to use --multirun|-m ?",
+        ),
+        (["test.param=1", "-m"], False, "1\n"),
+        (["test.param=1,2", "-m"], False, "1\n2\n"),
+    ],
+)
+def test_multirun_structured_conflict(
+    tmpdir: Any, overrides: List[str], error: bool, expected: Any
+) -> None:
+    cmd = [
+        sys.executable,
+        "tests/test_apps/multirun_structured_conflict/my_app.py",
+        "hydra/hydra_logging=disabled",
+        "hydra.sweep.dir=" + str(tmpdir),
+    ]
+    cmd.extend(overrides)
+    if error:
+        assert re.search(re.escape(expected), run_with_error(cmd)) is not None
+    else:
+        assert (
+            normalize_newlines(str(subprocess.check_output(cmd).decode("utf-8")))
+            == expected
+        )
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "sweep", [pytest.param(False, id="run"), pytest.param(True, id="sweep")]
+)
+def test_run_with_missing_default(tmpdir: Any, sweep: bool) -> None:
+    cmd = [
+        sys.executable,
+        "tests/test_apps/simple_app/my_app.py",
+        "--config-name=unspecified_mandatory_default",
+        "--config-path=../../../hydra/test_utils/configs",
+        "hydra/hydra_logging=disabled",
+        "hydra.sweep.dir=" + str(tmpdir),
+    ]
+    if sweep:
+        cmd.append("-m")
+    expected = """You must specify 'group1', e.g, group1=<OPTION>
+Available options:
+\tabc.cde
+\tfile1
+\tfile2"""
+    ret = run_with_error(cmd)
+    assert re.search(re.escape(expected), ret) is not None