Repository: getmoto/moto

Issue:
Bug - Batch - Error describing a job when it never ran successfully
When running a batch job, and Docker fails to start, describing the job results in this error:
AttributeError: 'Job' object has no attribute 'log_stream_name'

Due to this line:
https://github.com/spulec/moto/blob/master/moto/batch/models.py#L356

If Docker fails to start (or running the job fails due to any other reason), this attribute is never set.

Full error:
```
2020-02-28 23:16:27 - werkzeug     - INFO     - 127.0.0.1 - - [28/Feb/2020 23:16:27] "POST /v1/describejobs HTTP/1.1" 500 -
 2020-02-28 23:16:27 - werkzeug     - ERROR    - Error on request:
 Traceback (most recent call last):
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/werkzeug/serving.py", line 323, in run_wsgi
     execute(self.server.app)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/werkzeug/serving.py", line 312, in execute
     application_iter = app(environ, start_response)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/server.py", line 135, in __call__
     return backend_app(environ, start_response)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 2463, in __call__
     return self.wsgi_app(environ, start_response)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 2449, in wsgi_app
     response = self.handle_exception(e)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 1866, in handle_exception
     reraise(exc_type, exc_value, tb)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
     raise value
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
     response = self.full_dispatch_request()
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 1951, in full_dispatch_request
     rv = self.handle_user_exception(e)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 1820, in handle_user_exception
     reraise(exc_type, exc_value, tb)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
     raise value
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 1949, in full_dispatch_request
     rv = self.dispatch_request()
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/flask/app.py", line 1935, in dispatch_request
     return self.view_functions[rule.endpoint](**req.view_args)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/core/utils.py", line 144, in __call__
     result = self.callback(request, request.url, {})
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/core/responses.py", line 197, in dispatch
     return cls()._dispatch(*args, **kwargs)
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/core/responses.py", line 295, in _dispatch
     return self.call_action()
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/core/responses.py", line 380, in call_action
     response = method()
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/batch/responses.py", line 257, in describejobs
     return json.dumps({"jobs": self.batch_backend.describe_jobs(jobs)})
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/batch/models.py", line 1229, in describe_jobs
     result.append(job.describe())
   File "/builds/dazza-codes/python-notes/.venv/lib/python3.6/site-packages/moto/batch/models.py", line 355, in describe
     result["container"]["logStreamName"] = self.log_stream_name
 AttributeError: 'Job' object has no attribute 'log_stream_name'
```

Taken from #2775 


Discussion:

Resolving patch:
diff --git a/moto/batch/models.py b/moto/batch/models.py
--- a/moto/batch/models.py
+++ b/moto/batch/models.py
@@ -301,7 +301,7 @@ def __init__(self, name, job_def, job_queue, log_backend, container_overrides):
         self.job_name = name
         self.job_id = str(uuid.uuid4())
         self.job_definition = job_def
-        self.container_overrides = container_overrides
+        self.container_overrides = container_overrides or {}
         self.job_queue = job_queue
         self.job_state = "SUBMITTED"  # One of SUBMITTED | PENDING | RUNNABLE | STARTING | RUNNING | SUCCEEDED | FAILED
         self.job_queue.jobs.append(self)
@@ -317,6 +317,7 @@ def __init__(self, name, job_def, job_queue, log_backend, container_overrides):
 
         self.docker_client = docker.from_env()
         self._log_backend = log_backend
+        self.log_stream_name = None
 
         # Unfortunately mocking replaces this method w/o fallback enabled, so we
         # need to replace it if we detect it's been mocked
@@ -338,10 +339,11 @@ def describe(self):
             "jobId": self.job_id,
             "jobName": self.job_name,
             "jobQueue": self.job_queue.arn,
-            "startedAt": datetime2int(self.job_started_at),
             "status": self.job_state,
             "dependsOn": [],
         }
+        if result["status"] not in ["SUBMITTED", "PENDING", "RUNNABLE", "STARTING"]:
+            result["startedAt"] = datetime2int(self.job_started_at)
         if self.job_stopped:
             result["stoppedAt"] = datetime2int(self.job_stopped_at)
             result["container"] = {}
@@ -503,7 +505,10 @@ def run(self):
                 for line in logs_stdout + logs_stderr:
                     date, line = line.split(" ", 1)
                     date = dateutil.parser.parse(date)
-                    date = int(date.timestamp())
+                    # TODO: Replace with int(date.timestamp()) once we yeet Python2 out of the window
+                    date = int(
+                        (time.mktime(date.timetuple()) + date.microsecond / 1000000.0)
+                    )
                     logs.append({"timestamp": date, "message": line.strip()})
 
                 # Send to cloudwatch


Test patch:
diff --git a/tests/test_batch/test_batch.py b/tests/test_batch/test_batch.py
--- a/tests/test_batch/test_batch.py
+++ b/tests/test_batch/test_batch.py
@@ -10,17 +10,6 @@
 import nose
 
 
-def expected_failure(test):
-    @functools.wraps(test)
-    def inner(*args, **kwargs):
-        try:
-            test(*args, **kwargs)
-        except Exception as err:
-            raise nose.SkipTest
-
-    return inner
-
-
 DEFAULT_REGION = "eu-central-1"
 
 
@@ -692,7 +681,8 @@ def test_submit_job_by_name():
 
 
 # SLOW TESTS
-@expected_failure
+
+
 @mock_logs
 @mock_ec2
 @mock_ecs
@@ -720,13 +710,13 @@ def test_submit_job():
     queue_arn = resp["jobQueueArn"]
 
     resp = batch_client.register_job_definition(
-        jobDefinitionName="sleep10",
+        jobDefinitionName="sayhellotomylittlefriend",
         type="container",
         containerProperties={
-            "image": "busybox",
+            "image": "busybox:latest",
             "vcpus": 1,
             "memory": 128,
-            "command": ["sleep", "10"],
+            "command": ["echo", "hello"],
         },
     )
     job_def_arn = resp["jobDefinitionArn"]
@@ -740,13 +730,6 @@ def test_submit_job():
 
     while datetime.datetime.now() < future:
         resp = batch_client.describe_jobs(jobs=[job_id])
-        print(
-            "{0}:{1} {2}".format(
-                resp["jobs"][0]["jobName"],
-                resp["jobs"][0]["jobId"],
-                resp["jobs"][0]["status"],
-            )
-        )
 
         if resp["jobs"][0]["status"] == "FAILED":
             raise RuntimeError("Batch job failed")
@@ -763,10 +746,9 @@ def test_submit_job():
     resp = logs_client.get_log_events(
         logGroupName="/aws/batch/job", logStreamName=ls_name
     )
-    len(resp["events"]).should.be.greater_than(5)
+    [event["message"] for event in resp["events"]].should.equal(["hello"])
 
 
-@expected_failure
 @mock_logs
 @mock_ec2
 @mock_ecs
@@ -794,13 +776,13 @@ def test_list_jobs():
     queue_arn = resp["jobQueueArn"]
 
     resp = batch_client.register_job_definition(
-        jobDefinitionName="sleep10",
+        jobDefinitionName="sleep5",
         type="container",
         containerProperties={
-            "image": "busybox",
+            "image": "busybox:latest",
             "vcpus": 1,
             "memory": 128,
-            "command": ["sleep", "10"],
+            "command": ["sleep", "5"],
         },
     )
     job_def_arn = resp["jobDefinitionArn"]
@@ -843,7 +825,6 @@ def test_list_jobs():
     len(resp_finished_jobs2["jobSummaryList"]).should.equal(2)
 
 
-@expected_failure
 @mock_logs
 @mock_ec2
 @mock_ecs
@@ -874,7 +855,7 @@ def test_terminate_job():
         jobDefinitionName="sleep10",
         type="container",
         containerProperties={
-            "image": "busybox",
+            "image": "busybox:latest",
             "vcpus": 1,
             "memory": 128,
             "command": ["sleep", "10"],