Repository: locustio/locust

Issue:
SequentialTaskSet is broken when using local class members (headless mode)
<!-- 
If you have a general question about how to use Locust, please check Stack Overflow first https://stackoverflow.com/questions/tagged/locust

You can also ask new questions on SO, https://stackoverflow.com/questions/ask just remember to tag your question with "locust". Do not immediately post your issue here after posting to SO, wait for an answer there instead.

Use this form only for reporting actual bugs in locust. Be mindful that the developers of locust are unpaid volunteers, so make sure you have tried everything you can think of before filing a bug :) 
-->

### Describe the bug
Trying to execute a SequentialTaskSet in headless mode, store result's from first task, and use it in second task

### Expected behavior
Execute first task, retrieve json, store it as dict in SequentialTaskSet object, execute second task and access the variable.

### Actual behavior
Execute first task, json is retrieved, stored. In second task upon trying to access the stored data, I get following exceptions:
1.) AttributeError: 'dict' object has no attribute '_state'
2.) AttributeError: 'dict' object has no attribute 'environment'

### Steps to reproduce
Start provided mockserver.py (flask) on localhost:5000, then run provided locust file (sequence_test.py) with command lines specified



### Environment

- OS: 
Ubuntu 18.04.4 LTS running in WSL
 Linux PSALA059 4.4.0-18990-Microsoft #1-Microsoft Sat Sep 21 17:04:00 PST 2019 x86_64 x86_64 x86_64 GNU/Linux
- Python version:
3.6.9
- Locust version:
locust 1.0.1
- Locust command line that you ran:
run master locust:
` locust -f sequence_test.py --host=http://localhost:5000 --master --expect-workers=1 --headless -u 1 -r 1 -t 1h`

run worker locust:
`locust -f sequence_test.py --worker --master-host=127.0.0.1 --host=http://localhost:5000`

- Locust file contents (anonymized if necessary):
[locust-1.0.1-sequence-bug.zip](https://github.com/locustio/locust/files/4641968/locust-1.0.1-sequence-bug.zip)



Discussion:
Hi! I think the problem is that you're overwriting the self.user variable. It refers to the User class (it used to be self.locust). Name it anything else and it should work.
We could prevent this kind of issues by making user a read-only property. Probably wasnt as big a problem when the variable name was more uncommon (locust), but now I can definitely see more people running in to this.

Something like this.

```
class TaskSet(object):
    def __init__(self, parent):
        self._user = ... # instead of self.user = ...

    @property
    def user(self):
        return self._user
```
ooooh I see :) yeah it makes sense, I'll rename it
EDIT: it works now, thanks!

Resolving patch:
diff --git a/locust/user/task.py b/locust/user/task.py
--- a/locust/user/task.py
+++ b/locust/user/task.py
@@ -218,25 +218,19 @@ class ForumPage(TaskSet):
     if not set on the TaskSet.
     """
 
-    user = None
-    """Will refer to the root User class instance when the TaskSet has been instantiated"""
-
-    parent = None
-    """
-    Will refer to the parent TaskSet, or User, class instance when the TaskSet has been 
-    instantiated. Useful for nested TaskSet classes.
-    """
+    _user = None
+    _parent = None
 
     def __init__(self, parent):
         self._task_queue = []
         self._time_start = time()
         
         if isinstance(parent, TaskSet):
-            self.user = parent.user
+            self._user = parent.user
         else:
-            self.user = parent
+            self._user = parent
 
-        self.parent = parent
+        self._parent = parent
         
         # if this class doesn't have a min_wait, max_wait or wait_function defined, copy it from Locust
         if not self.min_wait:
@@ -246,9 +240,21 @@ def __init__(self, parent):
         if not self.wait_function:
             self.wait_function = self.user.wait_function
 
+
+
+    @property
+    def user(self):
+        """:py:class:`User <locust.User>` instance that this TaskSet was created by"""
+        return self._user
+
+    @property
+    def parent(self):
+        """Parent TaskSet instance of this TaskSet (or :py:class:`User <locust.User>` if this is not a nested TaskSet)"""
+        return self._parent
+
     def on_start(self):
         """
-        Called when a User starts executing (enters) this TaskSet
+        Called when a User starts executing this TaskSet
         """
         pass
     
@@ -392,8 +398,7 @@ def interrupt(self, reschedule=True):
     @property
     def client(self):
         """
-        Reference to the :py:attr:`client <locust.User.client>` attribute of the root
-        User instance.
+        Shortcut to the client :py:attr:`client <locust.User.client>` attribute of this TaskSet's :py:class:`User <locust.User>`
         """
         return self.user.client
 


Test patch:
diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py
--- a/locust/test/test_locust_class.py
+++ b/locust/test/test_locust_class.py
@@ -383,6 +383,25 @@ class MyUser(User):
         self.assertTrue(isinstance(parents["sub"], RootTaskSet))
         self.assertTrue(isinstance(parents["subsub"], SubTaskSet))
 
+    def test_user_is_read_only(self):
+        class MyTaskSet(TaskSet):
+            raised_attribute_error = False
+            @task
+            def overwrite_user(self):
+                try:
+                    self.user = "whatever"
+                except AttributeError:
+                    MyTaskSet.raised_attribute_error = True
+                raise StopUser()
+
+        class MyUser(User):
+            wait_time = constant(0)
+            host = ""
+            tasks = [MyTaskSet]
+        
+        MyUser(Environment()).run()
+        self.assertTrue(MyTaskSet.raised_attribute_error)
+
 
 class TestLocustClass(LocustTestCase):
     def test_locust_wait(self):