Repository: azavea/raster-vision

Issue:
Implement `--dry-run` / `-n` option in CLI
We should have a `--dry-run`/`-n` option in the CLI which will print information about the commands to be run, but not actually run them. The CLI option exists, but needs to be implemented in the code.


Discussion:
In progress.
There are some incoming changes that should help this effort:

https://github.com/azavea/raster-vision/pull/413/files#diff-c9f912f218017f4453e836b4eee86210R66

Keeps track of skipped commands, so that they can be reported during a dry run. Commands are skipped either because they are the same as other experiment's commands, or because their output already exists.

Resolving patch:
diff --git a/src/rastervision/cli/main.py b/src/rastervision/cli/main.py
--- a/src/rastervision/cli/main.py
+++ b/src/rastervision/cli/main.py
@@ -130,7 +130,8 @@ def run(runner, commands, experiment_module, dry_run, skip_file_check, arg,
         experiments,
         commands_to_run=commands,
         rerun_commands=rerun,
-        skip_file_check=skip_file_check)
+        skip_file_check=skip_file_check,
+        dry_run=dry_run)
 
 
 @main.command()
diff --git a/src/rastervision/runner/experiment_runner.py b/src/rastervision/runner/experiment_runner.py
--- a/src/rastervision/runner/experiment_runner.py
+++ b/src/rastervision/runner/experiment_runner.py
@@ -1,7 +1,8 @@
+import rastervision as rv
+
 from abc import (ABC, abstractmethod)
 from typing import (List, Union)
 
-import rastervision as rv
 from rastervision.runner import (CommandDefinition, CommandDAG)
 
 
@@ -10,15 +11,28 @@ def run(self,
             experiments: Union[List[rv.ExperimentConfig], rv.ExperimentConfig],
             commands_to_run=rv.ALL_COMMANDS,
             rerun_commands=False,
-            skip_file_check=False):
+            skip_file_check=False,
+            dry_run: bool = False):
         if not isinstance(experiments, list):
             experiments = [experiments]
 
-        command_definitions = CommandDefinition.from_experiments(experiments)
+        _command_definitions = CommandDefinition.from_experiments(experiments)
 
         # Filter  out commands we aren't running.
         command_definitions = CommandDefinition.filter_commands(
-            command_definitions, commands_to_run)
+            _command_definitions, commands_to_run)
+
+        # Print unrequested commands
+        if dry_run:
+            not_requested = set(_command_definitions) - set(command_definitions)
+            if not_requested:
+                print()
+                print('Not requsted:')
+                for command in not_requested:
+                    command_type = command.command_config.command_type
+                    experiment_id = command.experiment_id
+                    print('{} from {}'.format(command_type, experiment_id))
+                print()
 
         # Check if there are any unsatisfied inputs.
         missing_inputs = CommandDefinition.get_missing_inputs(
@@ -63,9 +77,17 @@ def run(self,
         command_dag = CommandDAG(
             unique_commands, rerun_commands, skip_file_check=skip_file_check)
 
-        if skipped_commands or command_dag.skipped_commands:
-            # TODO: Report skipped commands, either in the dry run or elsewhere.
-            pass
+        # Print conflicating or alread fulfilled commands
+        if dry_run:
+            skipped_commands.extend(command_dag.skipped_commands)
+            if skipped_commands:
+                print()
+                print('Skipped due to output conflicts:')
+                for command in skipped_commands:
+                    command_type = command.command_config.command_type
+                    experiment_id = command.experiment_id
+                    print('{} from {}'.format(command_type, experiment_id))
+                print()
 
         # Save experiment configs
         experiments_by_id = dict(map(lambda e: (e.id, e), experiments))
@@ -74,9 +96,19 @@ def run(self,
             if command_def.experiment_id not in seen_ids:
                 seen_ids.add(command_def.experiment_id)
                 experiment = experiments_by_id[command_def.experiment_id]
-                experiment.fully_resolve().save_config()
-
-        self._run_experiment(command_dag)
+                if not dry_run:
+                    experiment.fully_resolve().save_config()
+
+        if dry_run:
+            print()
+            print('To be run:')
+            for command in command_dag.get_sorted_commands():
+                command_type = command.command_type
+                root_uri = command.root_uri
+                print('{} in {}'.format(command_type, root_uri))
+            print()
+        else:
+            self._run_experiment(command_dag)
 
     @abstractmethod
     def _run_experiment(self, command_dag):


Test patch: