Repository: common-workflow-language/cwltool

Issue:
CWLTool --validate error should highlight offending tool on error.
## Expected Behavior
Tell us what should happen

When a workflow fails validation because of a tool not being configured correctly, the validation error should highlight which tool has failed.

## Actual Behavior
Tell us what happens instead

## Workflow Code
I have taken from https://github.com/common-workflow-language/cwl-v1.1/blob/main/tests/count-lines1-wf.cwl

```yaml
#!/usr/bin/env cwl-runner
class: Workflow
cwlVersion: v1.1

inputs:
  file1:
    type: File

outputs:
  count_output:
    type: int
    outputSource: step2/output

steps:
  step1:
    run: wc-tool.cwl
    in:
      file1: file1
    out: [output]

  step2:
    run: parseInt-tool.cwl
    in:
      file1: step1/output
    out: [output]
```

In the tool `wc-tool` I have created an invalid 'hints' section:

```yaml
#!/usr/bin/env cwl-runner

class: CommandLineTool
cwlVersion: v1.1

hints:

inputs:
  file1: File

outputs:
  output:
    type: File
    outputBinding: { glob: output }

baseCommand: [wc, -l]

stdin: $(inputs.file1.path)
stdout: output
```

## Full Traceback
```pytb
INFO /home/alexiswl/anaconda3/envs/cwl/bin/cwltool 3.0.20210319143721
INFO Resolved 'count-lines1-wf.cwl' to 'file:///c/Users/awluc/OneDrive/GitHub/UMCCR-ILLUMINA/cwl-iap/share/validate/count-lines1-wf.cwl'
ERROR I'm sorry, I couldn't load this CWL file.
The error was:
Traceback (most recent call last):
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/main.py", line 1035, in main
    tool = make_tool(uri, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/load_tool.py", line 460, in make_tool
    tool = loadingContext.construct_tool_object(processobj, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/workflow.py", line 51, in default_make_tool
    return Workflow(toolpath_object, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/workflow.py", line 105, in __init__
    self.make_workflow_step(
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/workflow.py", line 150, in make_workflow_step
    return WorkflowStep(toolpath_object, pos, loadingContext, parentworkflowProv)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/workflow.py", line 233, in __init__
    self.embedded_tool = load_tool(toolpath_object["run"], loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/load_tool.py", line 482, in load_tool
    return make_tool(uri, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/load_tool.py", line 460, in make_tool
    tool = loadingContext.construct_tool_object(processobj, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/workflow.py", line 47, in default_make_tool
    return command_line_tool.CommandLineTool(toolpath_object, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/command_line_tool.py", line 381, in __init__
    super().__init__(toolpath_object, loadingContext)
  File "/home/alexiswl/anaconda3/envs/cwl/lib/python3.8/site-packages/cwltool/process.py", line 605, in __init__
    self.hints.extend(self.tool.get("hints", []))
TypeError: 'NoneType' object is not iterable
```

## Your Environment
* cwltool version: 3.0.20210319143721
Check using ``cwltool --version``



Discussion:

Resolving patch:
diff --git a/cwltool/process.py b/cwltool/process.py
--- a/cwltool/process.py
+++ b/cwltool/process.py
@@ -578,7 +578,15 @@ def __init__(
         self.names = make_avro_schema([SCHEMA_FILE, SCHEMA_DIR, SCHEMA_ANY], Loader({}))
         self.tool = toolpath_object
         self.requirements = copy.deepcopy(getdefault(loadingContext.requirements, []))
-        self.requirements.extend(self.tool.get("requirements", []))
+        tool_requirements = self.tool.get("requirements", [])
+        if tool_requirements is None:
+            raise ValidationException(
+                SourceLine(self.tool, "requirements").makeError(
+                    "If 'requirements' is present then it must be a list "
+                    "or map/dictionary, not empty."
+                )
+            )
+        self.requirements.extend(tool_requirements)
         if "id" not in self.tool:
             self.tool["id"] = "_:" + str(uuid.uuid4())
         self.requirements.extend(
@@ -590,7 +598,15 @@ def __init__(
             )
         )
         self.hints = copy.deepcopy(getdefault(loadingContext.hints, []))
-        self.hints.extend(self.tool.get("hints", []))
+        tool_hints = self.tool.get("hints", [])
+        if tool_hints is None:
+            raise ValidationException(
+                SourceLine(self.tool, "hints").makeError(
+                    "If 'hints' is present then it must be a list "
+                    "or map/dictionary, not empty."
+                )
+            )
+        self.hints.extend(tool_hints)
         # Versions of requirements and hints which aren't mutated.
         self.original_requirements = copy.deepcopy(self.requirements)
         self.original_hints = copy.deepcopy(self.hints)


Test patch:
diff --git a/tests/test_examples.py b/tests/test_examples.py
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -1398,3 +1398,23 @@ def test_scatter_output_filenames(tmp_path: Path) -> None:
             and locations[1].endswith("output.txt_2")
             and locations[2].endswith("output.txt_3")
         ), f"Locations {locations} do not end with {output_names}"
+
+
+def test_malformed_hints() -> None:
+    """Confirm that empty hints section is caught."""
+    factory = cwltool.factory.Factory()
+    with pytest.raises(
+        ValidationException,
+        match=r".*wc-tool-bad-hints\.cwl:6:1: If 'hints' is\s*present\s*then\s*it\s*must\s*be\s*a\s*list.*",
+    ):
+        factory.make(get_data("tests/wc-tool-bad-hints.cwl"))
+
+
+def test_malformed_reqs() -> None:
+    """Confirm that empty reqs section is caught."""
+    factory = cwltool.factory.Factory()
+    with pytest.raises(
+        ValidationException,
+        match=r".*wc-tool-bad-reqs\.cwl:6:1: If 'requirements' is\s*present\s*then\s*it\s*must\s*be\s*a\s*list.*",
+    ):
+        factory.make(get_data("tests/wc-tool-bad-reqs.cwl"))
diff --git a/tests/wc-tool-bad-hints.cwl b/tests/wc-tool-bad-hints.cwl
new file mode 100644
--- /dev/null
+++ b/tests/wc-tool-bad-hints.cwl
@@ -0,0 +1,19 @@
+#!/usr/bin/env cwl-runner
+
+class: CommandLineTool
+cwlVersion: v1.1
+
+hints:
+
+inputs:
+  file1: File
+
+outputs:
+  output:
+    type: File
+    outputBinding: { glob: output }
+
+baseCommand: [wc, -l]
+
+stdin: $(inputs.file1.path)
+stdout: output
diff --git a/tests/wc-tool-bad-reqs.cwl b/tests/wc-tool-bad-reqs.cwl
new file mode 100644
--- /dev/null
+++ b/tests/wc-tool-bad-reqs.cwl
@@ -0,0 +1,19 @@
+#!/usr/bin/env cwl-runner
+
+class: CommandLineTool
+cwlVersion: v1.1
+
+requirements:
+
+inputs:
+  file1: File
+
+outputs:
+  output:
+    type: File
+    outputBinding: { glob: output }
+
+baseCommand: [wc, -l]
+
+stdin: $(inputs.file1.path)
+stdout: output