diff --git a/docs/install.rst b/docs/install.rst
index ef8af7e6..592ad278 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -69,7 +69,12 @@ We recommend that you use `Homebrew <https://brew.sh/>`_::
 git
 ~~~
 
-``git 1.9`` or better is required. Verify with ``git --version``.
+Git is not required if **all** of the following conditions are met:
+
+* You are using Rally only as a load generator (``--pipeline=benchmark-only``) or you are referring to Elasticsearch configurations with ``--team-path``.
+* You create your own tracks and refer to them with ``--track-path``.
+
+In all other cases, Rally requires ``git 1.9`` or better. Verify with ``git --version``.
 
 **Debian / Ubuntu**
 
@@ -139,16 +144,6 @@ Whenever you want to use Rally, run the activation script (step 2 above) first.
 
 .. _install_offline-install:
 
-Kubernetes Job
---------------
-
-You can run Rally as a Kubernetes `Job <https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/>`_ via `this <https://github.com/gdmello/elasticsearch-rally>`_ `Helm Chart <https://helm.sh/>`_.
-
-Docker
-------
-
-You can run Rally as a docker container too. Follow the instructions `here <https://github.com/gdmello/elasticsearch-rally/tree/master/docker>`_.
-
 Offline Install
 ---------------
 
diff --git a/esrally/config.py b/esrally/config.py
index 694520b6..7d6df330 100644
--- a/esrally/config.py
+++ b/esrally/config.py
@@ -106,6 +106,8 @@ def auto_load_local_config(base_config, additional_sections=None, config_file_cl
 
 
 class Config:
+    EARLIEST_SUPPORTED_VERSION = 12
+
     CURRENT_CONFIG_VERSION = 15
 
     """
@@ -586,6 +588,10 @@ class Prompter:
 
 
 def migrate(config_file, current_version, target_version, out=print, i=input):
+    if current_version < Config.EARLIEST_SUPPORTED_VERSION:
+        raise ConfigError("The config file in {} is too old. Please delete it and reconfigure Rally from scratch with {} configure."
+                          .format(config_file.location, PROGRAM_NAME))
+
     prompter = Prompter(i=i, o=out, assume_defaults=False)
     logger.info("Upgrading configuration from version [%s] to [%s]." % (current_version, target_version))
     # Something is really fishy. We don't want to downgrade the configuration.
@@ -596,178 +602,6 @@ def migrate(config_file, current_version, target_version, out=print, i=input):
     config_file.backup()
     config = config_file.load(interpolation=None)
 
-    if current_version == 0 and target_version > current_version:
-        logger.info("Migrating config from version [0] to [1]")
-        current_version = 1
-        config["meta"] = {}
-        config["meta"]["config.version"] = str(current_version)
-        # in version 1 we changed some directories from being absolute to being relative
-        config["system"]["log.root.dir"] = "logs"
-        config["provisioning"]["local.install.dir"] = "install"
-        config["reporting"]["report.base.dir"] = "reports"
-    if current_version == 1 and target_version > current_version:
-        logger.info("Migrating config from version [1] to [2]")
-        current_version = 2
-        config["meta"]["config.version"] = str(current_version)
-        # no need to ask the user now if we are about to upgrade to version 4
-        config["reporting"]["datastore.type"] = "in-memory"
-        config["reporting"]["datastore.host"] = ""
-        config["reporting"]["datastore.port"] = ""
-        config["reporting"]["datastore.secure"] = ""
-        config["reporting"]["datastore.user"] = ""
-        config["reporting"]["datastore.password"] = ""
-        config["system"]["env.name"] = "local"
-    if current_version == 2 and target_version > current_version:
-        logger.info("Migrating config from version [2] to [3]")
-        current_version = 3
-        config["meta"]["config.version"] = str(current_version)
-        # Remove obsolete settings
-        config["reporting"].pop("report.base.dir")
-        config["reporting"].pop("output.html.report.filename")
-    if current_version == 3 and target_version > current_version:
-        root_dir = config["system"]["root.dir"]
-        out(
-            """
-            *****************************************************************************************
-
-            You have an old configuration of Rally. Rally has now a much simpler setup
-            routine which will autodetect lots of settings for you and it also does not
-            require you to setup a metrics store anymore.
-
-            Rally will now migrate your configuration but if you don't need advanced features
-            like a metrics store, then you should delete the configuration directory:
-
-              rm -rf {0}
-
-            and then rerun Rally's configuration routine:
-
-              {1} configure
-
-            Please also note you have {2:.1f} GB of data in your current benchmark directory at
-
-              {3}
-
-            You might want to clean up this directory also.
-
-            For more details please see {4}
-
-            *****************************************************************************************
-
-            Pausing for 10 seconds to let you consider this message.
-            """.format(config_file.config_dir,
-                       PROGRAM_NAME,
-                       convert.bytes_to_gb(io.get_size(root_dir)),
-                       root_dir,
-                       console.format.link("https://github.com/elastic/rally/blob/master/CHANGELOG.md#030")))
-        time.sleep(10)
-        logger.info("Migrating config from version [3] to [4]")
-        current_version = 4
-        config["meta"]["config.version"] = str(current_version)
-        if len(config["reporting"]["datastore.host"]) > 0:
-            config["reporting"]["datastore.type"] = "elasticsearch"
-        else:
-            config["reporting"]["datastore.type"] = "in-memory"
-        # Remove obsolete settings
-        config["build"].pop("maven.bin")
-        config["benchmarks"].pop("metrics.stats.disk.device")
-
-    if current_version == 4 and target_version > current_version:
-        config["tracks"] = {}
-        config["tracks"]["default.url"] = "https://github.com/elastic/rally-tracks"
-        current_version = 5
-        config["meta"]["config.version"] = str(current_version)
-
-    if current_version == 5 and target_version > current_version:
-        config["defaults"] = {}
-        config["defaults"]["preserve_benchmark_candidate"] = str(False)
-        current_version = 6
-        config["meta"]["config.version"] = str(current_version)
-
-    if current_version == 6 and target_version > current_version:
-        # Remove obsolete settings
-        config.pop("provisioning")
-        config["system"].pop("log.root.dir")
-        current_version = 7
-        config["meta"]["config.version"] = str(current_version)
-
-    if current_version == 7 and target_version > current_version:
-        # move [system][root.dir] to [node][root.dir]
-        if "node" not in config:
-            config["node"] = {}
-        config["node"]["root.dir"] = config["system"].pop("root.dir")
-        # also move all references!
-        for section in config:
-            for k, v in config[section].items():
-                config[section][k] = v.replace("${system:root.dir}", "${node:root.dir}")
-        current_version = 8
-        config["meta"]["config.version"] = str(current_version)
-    if current_version == 8 and target_version > current_version:
-        config["teams"] = {}
-        config["teams"]["default.url"] = "https://github.com/elastic/rally-teams"
-        current_version = 9
-        config["meta"]["config.version"] = str(current_version)
-    if current_version == 9 and target_version > current_version:
-        config["distributions"] = {}
-        config["distributions"]["release.1.url"] = "https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-" \
-                                                   "{{VERSION}}.tar.gz"
-        config["distributions"]["release.2.url"] = "https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/" \
-                                                   "distribution/tar/elasticsearch/{{VERSION}}/elasticsearch-{{VERSION}}.tar.gz"
-        config["distributions"]["release.url"] = "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-{{VERSION}}.tar.gz"
-        config["distributions"]["release.cache"] = "true"
-        current_version = 10
-        config["meta"]["config.version"] = str(current_version)
-    if current_version == 10 and target_version > current_version:
-        config["runtime"]["java.home"] = config["runtime"].pop("java8.home")
-        current_version = 11
-        config["meta"]["config.version"] = str(current_version)
-    if current_version == 11 and target_version > current_version:
-        # As this is a rather complex migration, we log more than usual to understand potential migration problems better.
-        if "source" in config:
-            if "local.src.dir" in config["source"]:
-                previous_root = config["source"].pop("local.src.dir")
-                logger.info("Set [source][local.src.dir] to [%s]." % previous_root)
-                # if this directory was Rally's default location, then move it on the file system to allow for checkouts of plugins
-                # in the sibling directory.
-                if previous_root == os.path.join(config["node"]["root.dir"], "src"):
-                    new_root_dir_all_sources = previous_root
-                    new_es_sub_dir = "elasticsearch"
-                    new_root = os.path.join(new_root_dir_all_sources, new_es_sub_dir)
-                    # only attempt to move if the directory exists. It may be possible that users never ran a source benchmark although they
-                    # have configured it. In that case the source directory will not yet exist.
-                    if io.exists(previous_root):
-                        logger.info("Previous source directory was at Rally's default location [%s]. Moving to [%s]."
-                                    % (previous_root, new_root))
-                        try:
-                            # we need to do this in two steps as we need to move the sources to a subdirectory
-                            tmp_path = io.normalize_path(os.path.join(new_root_dir_all_sources, os.pardir, "tmp_src_mig"))
-                            os.rename(previous_root, tmp_path)
-                            io.ensure_dir(new_root)
-                            os.rename(tmp_path, new_root)
-                        except OSError:
-                            logger.exception("Could not move source directory from [%s] to [%s]." % (previous_root, new_root))
-                            # A warning is sufficient as Rally should just do a fresh checkout if moving did not work.
-                            console.warn("Elasticsearch source directory could not be moved from [%s] to [%s]. Please check the logs."
-                                         % (previous_root, new_root))
-                    else:
-                        logger.info("Source directory is configured at Rally's default location [%s] but does not exist yet."
-                                    % previous_root)
-                else:
-                    logger.info("Previous source directory was the custom directory [%s]." % previous_root)
-                    new_root_dir_all_sources = io.normalize_path(os.path.join(previous_root, os.path.pardir))
-                    # name of the elasticsearch project directory.
-                    new_es_sub_dir = io.basename(previous_root)
-
-                logger.info("Setting [node][src.root.dir] to [%s]." % new_root_dir_all_sources)
-                config["node"]["src.root.dir"] = new_root_dir_all_sources
-                logger.info("Setting [source][elasticsearch.src.subdir] to [%s]" % new_es_sub_dir)
-                config["source"]["elasticsearch.src.subdir"] = new_es_sub_dir
-            else:
-                logger.info("Key [local.src.dir] not found. Advancing without changes.")
-        else:
-            logger.info("No section named [source] found in config. Advancing without changes.")
-        current_version = 12
-        config["meta"]["config.version"] = str(current_version)
-
     if current_version == 12 and target_version > current_version:
         # the current configuration allows to benchmark from sources
         if "build" in config and "gradle.bin" in config["build"]:
diff --git a/esrally/driver/runner.py b/esrally/driver/runner.py
index 49badf3e..4ba93457 100644
--- a/esrally/driver/runner.py
+++ b/esrally/driver/runner.py
@@ -523,7 +523,7 @@ class Query(Runner):
             "weight": retrieved_pages,
             "pages": retrieved_pages,
             "hits": hits,
-            "unit": "ops",
+            "unit": "pages",
             "timed_out": timed_out,
             "took": took
         }
diff --git a/esrally/utils/git.py b/esrally/utils/git.py
index e5f885e4..b5d044e8 100644
--- a/esrally/utils/git.py
+++ b/esrally/utils/git.py
@@ -8,7 +8,8 @@ from esrally.utils import io, process
 def probed(f):
     def probe(src, *args, **kwargs):
         # Probe for -C
-        if not process.exit_status_as_bool(lambda: process.run_subprocess_with_logging("git -C %s --version" % src, level=logging.DEBUG)):
+        if not process.exit_status_as_bool(lambda: process.run_subprocess_with_logging(
+                "git -C {} --version".format(src), level=logging.DEBUG), quiet=True):
             version = process.run_subprocess_with_output("git --version")
             if version:
                 version = str(version).strip()
@@ -25,7 +26,7 @@ def is_working_copy(src):
     :param src: A directory. May or may not exist.
     :return: True iff the given directory is a git working copy.
     """
-    return os.path.exists(src) and os.path.exists("%s/.git" % src)
+    return os.path.exists(src) and os.path.exists(os.path.join(src, ".git"))
 
 
 def clone(src, remote):
diff --git a/esrally/utils/process.py b/esrally/utils/process.py
index 068c32d4..c927396e 100644
--- a/esrally/utils/process.py
+++ b/esrally/utils/process.py
@@ -28,17 +28,19 @@ def run_subprocess_with_output(command_line):
     return lines
 
 
-def exit_status_as_bool(runnable):
+def exit_status_as_bool(runnable, quiet=False):
     """
 
     :param runnable: A runnable returning an int as exit status assuming ``0`` is meaning success.
+    :param quiet: Suppress any output (default: False).
     :return: True iff the runnable has terminated successfully.
     """
     try:
         return_code = runnable()
         return return_code == 0 or return_code is None
     except OSError:
-        logger.exception("Could not execute command.")
+        if not quiet:
+            logger.exception("Could not execute command.")
         return False
 
 

Don't require git
As Rally allows recently to define tracks as simple files there is no hard requirement that git is installed (but obviously usage is restricted to Rally as load generator with custom tracks).
diff --git a/tests/config_test.py b/tests/config_test.py
index a368b47e..43ba46d2 100644
--- a/tests/config_test.py
+++ b/tests/config_test.py
@@ -419,11 +419,7 @@ class ConfigFactoryTests(TestCase):
 
 
 class ConfigMigrationTests(TestCase):
-    # catch all test, migrations are checked in more detail in the other tests
-    @mock.patch("esrally.utils.io.get_size")
-    @mock.patch("esrally.time.sleep")
-    def test_migrate_from_0_to_latest(self, sleep, get_size):
-        get_size.return_value = 0
+    def test_does_not_migrate_outdated_config(self):
         config_file = InMemoryConfigStore("test")
         sample_config = {
             "system": {
@@ -448,347 +444,45 @@ class ConfigMigrationTests(TestCase):
         }
 
         config_file.store(sample_config)
-        config.migrate(config_file, 0, config.Config.CURRENT_CONFIG_VERSION, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual(str(config.Config.CURRENT_CONFIG_VERSION), config_file.config["meta"]["config.version"])
-
-    def test_migrate_from_2_to_3(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 2
-            },
-            "system": {
-                "root.dir": "in-memory"
-            },
-            "reporting": {
-                "report.base.dir": "/tests/rally/reporting",
-                "output.html.report.filename": "index.html"
-            },
-        }
-
-        config_file.store(sample_config)
-        config.migrate(config_file, 2, 3, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("3", config_file.config["meta"]["config.version"])
-        # Did not delete the section...
-        self.assertTrue("reporting" in config_file.config)
-        # ... but the key
-        self.assertFalse("report.base.dir" in config_file.config["reporting"])
-        self.assertFalse("output.html.report.filename" in config_file.config["reporting"])
+        with self.assertRaisesRegex(config.ConfigError, "The config file.*is too old. Please delete it and reconfigure Rally from scratch"):
+            config.migrate(config_file, config.Config.EARLIEST_SUPPORTED_VERSION - 1, config.Config.CURRENT_CONFIG_VERSION, out=null_output)
 
+    # catch all test, migrations are checked in more detail in the other tests
     @mock.patch("esrally.utils.io.get_size")
     @mock.patch("esrally.time.sleep")
-    def test_migrate_from_3_to_4(self, sleep, get_size):
+    def test_migrate_from_earliest_supported_to_latest(self, sleep, get_size):
         get_size.return_value = 0
         config_file = InMemoryConfigStore("test")
         sample_config = {
             "meta": {
-                "config.version": 3
+                "config.version": config.Config.EARLIEST_SUPPORTED_VERSION
             },
             "system": {
                 "root.dir": "in-memory"
             },
-            "reporting": {
-                "datastore.host": ""
+            "provisioning": {
+
             },
             "build": {
                 "maven.bin": "/usr/local/mvn"
             },
             "benchmarks": {
                 "metrics.stats.disk.device": "/dev/hdd1"
-            }
-        }
-
-        config_file.store(sample_config)
-        config.migrate(config_file, 3, 4, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("4", config_file.config["meta"]["config.version"])
-        # Did not delete the section...
-        self.assertTrue("build" in config_file.config)
-        # ... but the key
-        self.assertFalse("maven.bin" in config_file.config["build"])
-        self.assertTrue("benchmarks" in config_file.config)
-        self.assertFalse("metrics.stats.disk.device" in config_file.config["benchmarks"])
-        self.assertEqual("in-memory", config_file.config["reporting"]["datastore.type"])
-
-    def test_migrate_from_4_to_5(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 4
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 4, 5, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("5", config_file.config["meta"]["config.version"])
-        self.assertTrue("tracks" in config_file.config)
-        self.assertEqual("https://github.com/elastic/rally-tracks", config_file.config["tracks"]["default.url"])
-
-    def test_migrate_from_5_to_6(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 5
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 5, 6, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("6", config_file.config["meta"]["config.version"])
-        self.assertTrue("defaults" in config_file.config)
-        self.assertEqual("False", config_file.config["defaults"]["preserve_benchmark_candidate"])
-
-    def test_migrate_from_6_to_7(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 6
-            },
-            "system": {
-                "log.root.dir": "logs"
-            },
-            "provisioning": {
-                "local.install.dir": "install"
-            },
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 6, 7, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("7", config_file.config["meta"]["config.version"])
-        self.assertTrue("provisioning" not in config_file.config)
-        self.assertTrue("log.root.dir" not in config_file.config["system"])
-
-    def test_migrate_from_7_to_8(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 7
-            },
-            "system": {
-                "root.dir": "~/.rally/benchmarks",
-                "environment.name": "local"
-            },
-            "benchmarks": {
-                "local.dataset.cache": "${system:root.dir}/data",
-                "some.other.cache": "/data"
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 7, 8, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("8", config_file.config["meta"]["config.version"])
-        self.assertTrue("root.dir" not in config_file.config["system"])
-        self.assertEqual("~/.rally/benchmarks", config_file.config["node"]["root.dir"])
-        self.assertEqual("local", config_file.config["system"]["environment.name"])
-        self.assertEqual("${node:root.dir}/data", config_file.config["benchmarks"]["local.dataset.cache"])
-        self.assertEqual("/data", config_file.config["benchmarks"]["some.other.cache"])
-
-    def test_migrate_from_8_to_9(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 8
-            },
-            "system": {
-                "root.dir": "~/.rally/benchmarks",
-                "environment.name": "local"
             },
-            "benchmarks": {
-                "local.dataset.cache": "${system:root.dir}/data",
-                "some.other.cache": "/data"
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 8, 9, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("9", config_file.config["meta"]["config.version"])
-        self.assertTrue("teams" in config_file.config)
-        self.assertEqual("https://github.com/elastic/rally-teams", config_file.config["teams"]["default.url"])
-
-    def test_migrate_from_9_to_10(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 9
-            },
-            "system": {
-                "root.dir": "~/.rally/benchmarks",
-                "environment.name": "local"
-            },
-            "benchmarks": {
-                "local.dataset.cache": "${system:root.dir}/data",
-                "some.other.cache": "/data"
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 9, 10, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("10", config_file.config["meta"]["config.version"])
-        self.assertTrue("distributions" in config_file.config)
-        self.assertEqual("https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-{{VERSION}}.tar.gz",
-                         config_file.config["distributions"]["release.1.url"])
-        self.assertEqual("https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/"
-                         "{{VERSION}}/elasticsearch-{{VERSION}}.tar.gz",
-                         config_file.config["distributions"]["release.2.url"])
-        self.assertEqual("https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-{{VERSION}}.tar.gz",
-                         config_file.config["distributions"]["release.url"])
-        self.assertEqual("true",
-                         config_file.config["distributions"]["release.cache"])
-
-    def test_migrate_from_10_to_11(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 10
+            "reporting": {
+                "report.base.dir": "/tests/rally/reporting",
+                "output.html.report.filename": "index.html"
             },
             "runtime": {
                 "java8.home": "/opt/jdk/8",
             }
         }
-        config_file.store(sample_config)
-        config.migrate(config_file, 10, 11, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("11", config_file.config["meta"]["config.version"])
-        self.assertTrue("runtime" in config_file.config)
-        self.assertFalse("java8.home" in config_file.config["runtime"])
-        self.assertEqual("/opt/jdk/8", config_file.config["runtime"]["java.home"])
-
-    @mock.patch("esrally.utils.io.exists")
-    @mock.patch("os.rename")
-    def test_migrate_from_11_to_12_with_default_src_config_repo_checked_out(self, path_rename, path_exists):
-        path_exists.return_value = True
-
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 11
-            },
-            "node": {
-                "root.dir": io.normalize_path("~/.rally/benchmarks")
-            },
-            "source": {
-                "local.src.dir": io.normalize_path("~/.rally/benchmarks/src")
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 11, 12, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("12", config_file.config["meta"]["config.version"])
-        self.assertEqual(io.normalize_path("~/.rally/benchmarks/src"), config_file.config["node"]["src.root.dir"])
-        self.assertEqual("elasticsearch", config_file.config["source"]["elasticsearch.src.subdir"])
-
-        path_rename.assert_has_calls(
-            [
-                mock.call(io.normalize_path("~/.rally/benchmarks/src"), io.normalize_path("~/.rally/benchmarks/tmp_src_mig")),
-                mock.call(io.normalize_path("~/.rally/benchmarks/tmp_src_mig"),
-                          io.normalize_path("~/.rally/benchmarks/src/elasticsearch")),
-             ]
-        )
-
-    @mock.patch("esrally.utils.io.exists")
-    @mock.patch("os.rename")
-    def test_migrate_from_11_to_12_with_default_src_config_repo_not_checked_out(self, path_rename, path_exists):
-        path_exists.return_value = False
-
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 11
-            },
-            "node": {
-                "root.dir": io.normalize_path("~/.rally/benchmarks")
-            },
-            "source": {
-                "local.src.dir": io.normalize_path("~/.rally/benchmarks/src")
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 11, 12, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("12", config_file.config["meta"]["config.version"])
-        self.assertEqual(io.normalize_path("~/.rally/benchmarks/src"), config_file.config["node"]["src.root.dir"])
-        self.assertEqual("elasticsearch", config_file.config["source"]["elasticsearch.src.subdir"])
-        # did all the migrations but nothing moved
-        path_rename.assert_not_called()
-
-    def test_migrate_from_11_to_12_without_src_config(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 11
-            },
-            "node": {
-                "root.dir": "~/.rally/benchmarks"
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 11, 12, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("12", config_file.config["meta"]["config.version"])
-        self.assertFalse("src.root.dir" in config_file.config["node"])
-
-    def test_migrate_from_11_to_12_with_partial_src_config(self):
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 11
-            },
-            "node": {
-                "root.dir": "~/.rally/benchmarks"
-            },
-            "source": {
-                # a source config section without any keys should be treated like a missing source config section
-            }
-        }
-        config_file.store(sample_config)
-        config.migrate(config_file, 11, 12, out=null_output)
-
-        self.assertTrue(config_file.backup_created)
-        self.assertEqual("12", config_file.config["meta"]["config.version"])
-        self.assertFalse("src.root.dir" in config_file.config["node"])
-        self.assertFalse("elasticsearch.src.subdir" in config_file.config["source"])
 
-    @mock.patch("esrally.utils.io.exists")
-    @mock.patch("os.rename")
-    def test_migrate_from_11_to_12_with_custom_src_config(self, path_rename, path_exists):
-        path_exists.return_value = False
-
-        config_file = InMemoryConfigStore("test")
-        sample_config = {
-            "meta": {
-                "config.version": 11
-            },
-            "node": {
-                "root.dir": io.normalize_path("~/.rally/benchmarks")
-            },
-            "source": {
-                "local.src.dir": io.normalize_path("~/Projects/elasticsearch/master/es")
-            }
-        }
         config_file.store(sample_config)
-        config.migrate(config_file, 11, 12, out=null_output)
+        config.migrate(config_file, config.Config.EARLIEST_SUPPORTED_VERSION, config.Config.CURRENT_CONFIG_VERSION, out=null_output)
 
         self.assertTrue(config_file.backup_created)
-        self.assertEqual("12", config_file.config["meta"]["config.version"])
-        self.assertEqual(io.normalize_path("~/Projects/elasticsearch/master"), config_file.config["node"]["src.root.dir"])
-        self.assertEqual("es", config_file.config["source"]["elasticsearch.src.subdir"])
-        # did all the migrations but nothing moved
-        path_rename.assert_not_called()
+        self.assertEqual(str(config.Config.CURRENT_CONFIG_VERSION), config_file.config["meta"]["config.version"])
 
     def test_migrate_from_12_to_13_without_gradle(self):
         config_file = InMemoryConfigStore("test")
diff --git a/tests/driver/runner_test.py b/tests/driver/runner_test.py
index 55fc092c..8ad55c61 100644
--- a/tests/driver/runner_test.py
+++ b/tests/driver/runner_test.py
@@ -644,7 +644,7 @@ class QueryRunnerTests(TestCase):
         self.assertEqual(1, results["pages"])
         self.assertEqual(2, results["hits"])
         self.assertEqual(4, results["took"])
-        self.assertEqual("ops", results["unit"])
+        self.assertEqual("pages", results["unit"])
         self.assertFalse(results["timed_out"])
         self.assertFalse("error-type" in results)
 
@@ -692,7 +692,7 @@ class QueryRunnerTests(TestCase):
         self.assertEqual(1, results["pages"])
         self.assertEqual(2, results["hits"])
         self.assertEqual(4, results["took"])
-        self.assertEqual("ops", results["unit"])
+        self.assertEqual("pages", results["unit"])
         self.assertFalse(results["timed_out"])
         self.assertFalse("error-type" in results)
 
@@ -756,7 +756,7 @@ class QueryRunnerTests(TestCase):
         self.assertEqual(2, results["pages"])
         self.assertEqual(3, results["hits"])
         self.assertEqual(79, results["took"])
-        self.assertEqual("ops", results["unit"])
+        self.assertEqual("pages", results["unit"])
         self.assertTrue(results["timed_out"])
         self.assertFalse("error-type" in results)
 
@@ -812,7 +812,7 @@ class QueryRunnerTests(TestCase):
         self.assertEqual(2, results["weight"])
         self.assertEqual(2, results["pages"])
         self.assertEqual(1, results["hits"])
-        self.assertEqual("ops", results["unit"])
+        self.assertEqual("pages", results["unit"])
         self.assertEqual(55, results["took"])
         self.assertFalse("error-type" in results)
 
@@ -878,7 +878,7 @@ class QueryRunnerTests(TestCase):
         self.assertEqual(2, results["pages"])
         self.assertEqual(4, results["hits"])
         self.assertEqual(900, results["took"])
-        self.assertEqual("ops", results["unit"])
+        self.assertEqual("pages", results["unit"])
         self.assertFalse(results["timed_out"])
         self.assertFalse("error-type" in results)
 

pip3 install -e .
parse_log_pytest
pytest
pytest pytest-benchmark
apt-get update apt-get install -y gcc python3-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
elasticsearch==6.2.0
-e git+https://github.com/elastic/rally.git@fab6a504616af4b0de6584652e96bd30a466c246#egg=esrally
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==2.9.5
jsonschema==2.5.1
MarkupSafe==2.0.1
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
psutil==5.4.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
py-cpuinfo==3.2.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-benchmark==3.4.1
tabulate==0.8.1
thespian==3.9.2
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.22
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: rally
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - elasticsearch==6.2.0
      - jinja2==2.9.5
      - jsonschema==2.5.1
      - markupsafe==2.0.1
      - psutil==5.4.0
      - py-cpuinfo==3.2.0
      - pytest-benchmark==3.4.1
      - tabulate==0.8.1
      - thespian==3.9.2
      - urllib3==1.22
prefix: /opt/conda/envs/rally

tests/config_test.py::ConfigMigrationTests::test_does_not_migrate_outdated_config tests/config_test.py::ConfigMigrationTests::test_migrate_from_earliest_supported_to_latest tests/driver/runner_test.py::QueryRunnerTests::test_scroll_query_early_termination tests/driver/runner_test.py::QueryRunnerTests::test_scroll_query_only_one_page tests/driver/runner_test.py::QueryRunnerTests::test_scroll_query_only_one_page_only_request_body_defined tests/driver/runner_test.py::QueryRunnerTests::test_scroll_query_request_all_pages tests/driver/runner_test.py::QueryRunnerTests::test_scroll_query_with_explicit_number_of_pages
tests/config_test.py::ConfigTests::test_add_all_in_section tests/config_test.py::ConfigTests::test_load_all_opts_in_section tests/config_test.py::ConfigTests::test_load_existing_config tests/config_test.py::ConfigTests::test_load_non_existing_config tests/config_test.py::AutoLoadConfigTests::test_can_create_non_existing_config tests/config_test.py::AutoLoadConfigTests::test_can_load_and_amend_existing_config tests/config_test.py::AutoLoadConfigTests::test_can_migrate_outdated_config tests/config_test.py::ConfigFactoryTests::test_create_advanced_config tests/config_test.py::ConfigFactoryTests::test_create_simple_config tests/config_test.py::ConfigFactoryTests::test_create_simple_config_no_java_detected tests/config_test.py::ConfigFactoryTests::test_create_simple_config_no_java_installed tests/config_test.py::ConfigMigrationTests::test_migrate_from_12_to_13_with_gradle_and_jdk8_ask_user_and_skip tests/config_test.py::ConfigMigrationTests::test_migrate_from_12_to_13_with_gradle_and_jdk8_ask_user_enter_valid tests/config_test.py::ConfigMigrationTests::test_migrate_from_12_to_13_with_gradle_and_jdk8_autodetect_jdk9 tests/config_test.py::ConfigMigrationTests::test_migrate_from_12_to_13_with_gradle_and_jdk9 tests/config_test.py::ConfigMigrationTests::test_migrate_from_12_to_13_without_gradle tests/config_test.py::ConfigMigrationTests::test_migrate_from_13_to_14_with_gradle_and_jdk10 tests/config_test.py::ConfigMigrationTests::test_migrate_from_13_to_14_with_gradle_and_jdk8_ask_user_and_skip tests/config_test.py::ConfigMigrationTests::test_migrate_from_13_to_14_with_gradle_and_jdk8_ask_user_enter_valid tests/config_test.py::ConfigMigrationTests::test_migrate_from_13_to_14_with_gradle_and_jdk8_autodetect_jdk10 tests/config_test.py::ConfigMigrationTests::test_migrate_from_13_to_14_without_gradle tests/config_test.py::ConfigMigrationTests::test_migrate_from_14_to_15_with_gradle tests/config_test.py::ConfigMigrationTests::test_migrate_from_14_to_15_with_source_plugin_definition tests/config_test.py::ConfigMigrationTests::test_migrate_from_14_to_15_without_gradle tests/driver/runner_test.py::RegisterRunnerTests::test_runner_class_should_be_wrapped tests/driver/runner_test.py::RegisterRunnerTests::test_runner_class_with_context_manager_should_be_registered_as_is tests/driver/runner_test.py::RegisterRunnerTests::test_runner_function_should_be_wrapped tests/driver/runner_test.py::BulkIndexRunnerTests::test_bulk_index_error tests/driver/runner_test.py::BulkIndexRunnerTests::test_bulk_index_missing_params tests/driver/runner_test.py::BulkIndexRunnerTests::test_bulk_index_success_with_metadata tests/driver/runner_test.py::BulkIndexRunnerTests::test_bulk_index_success_without_metadata tests/driver/runner_test.py::BulkIndexRunnerTests::test_mixed_bulk_with_detailed_stats tests/driver/runner_test.py::BulkIndexRunnerTests::test_mixed_bulk_with_simple_stats tests/driver/runner_test.py::QueryRunnerTests::test_query_match_all tests/driver/runner_test.py::QueryRunnerTests::test_query_match_only_request_body_defined tests/driver/runner_test.py::PutPipelineRunnerTests::test_create_pipeline tests/driver/runner_test.py::PutPipelineRunnerTests::test_param_body_mandatory tests/driver/runner_test.py::ClusterHealthRunnerTests::test_accepts_better_cluster_status tests/driver/runner_test.py::ClusterHealthRunnerTests::test_rejects_relocating_shards tests/driver/runner_test.py::ClusterHealthRunnerTests::test_rejects_unknown_cluster_status tests/driver/runner_test.py::ClusterHealthRunnerTests::test_waits_for_expected_cluster_status tests/driver/runner_test.py::CreateIndexRunnerTests::test_creates_multiple_indices tests/driver/runner_test.py::CreateIndexRunnerTests::test_param_indices_mandatory tests/driver/runner_test.py::DeleteIndexRunnerTests::test_deletes_all_indices tests/driver/runner_test.py::DeleteIndexRunnerTests::test_deletes_existing_indices tests/driver/runner_test.py::CreateIndexTemplateRunnerTests::test_create_index_templates tests/driver/runner_test.py::CreateIndexTemplateRunnerTests::test_param_templates_mandatory tests/driver/runner_test.py::DeleteIndexTemplateRunnerTests::test_deletes_all_index_templates tests/driver/runner_test.py::DeleteIndexTemplateRunnerTests::test_deletes_only_existing_index_templates tests/driver/runner_test.py::DeleteIndexTemplateRunnerTests::test_param_templates_mandatory tests/driver/runner_test.py::RawRequestRunnerTests::test_issue_create_index tests/driver/runner_test.py::RawRequestRunnerTests::test_issue_delete_index tests/driver/runner_test.py::RawRequestRunnerTests::test_issue_msearch tests/driver/runner_test.py::RawRequestRunnerTests::test_issue_request_with_defaults tests/driver/runner_test.py::RetryTests::test_assumes_success_if_runner_returns_non_dict tests/driver/runner_test.py::RetryTests::test_does_not_retry_on_application_error_if_not_wanted tests/driver/runner_test.py::RetryTests::test_does_not_retry_on_timeout_if_not_wanted tests/driver/runner_test.py::RetryTests::test_is_does_not_retry_on_success tests/driver/runner_test.py::RetryTests::test_is_transparent_on_application_error_when_no_retries tests/driver/runner_test.py::RetryTests::test_is_transparent_on_exception_when_no_retries tests/driver/runner_test.py::RetryTests::test_is_transparent_on_success_when_no_retries tests/driver/runner_test.py::RetryTests::test_retries_mixed_timeout_and_application_errors tests/driver/runner_test.py::RetryTests::test_retries_on_application_error_if_wanted tests/driver/runner_test.py::RetryTests::test_retries_on_timeout_if_wanted_and_raises_if_no_recovery tests/driver/runner_test.py::RetryTests::test_retries_on_timeout_if_wanted_and_returns_first_call
docs/install.rst esrally/utils/git.py esrally/utils/process.py esrally/config.py esrally/driver/runner.py
docs/install.rst esrally/utils/git.py esrally/utils/process.py esrally/config.py esrally/driver/runner.py