Repository: oppia/oppia

Issue:
ExplorationStateIdMappingJob fails in production
The ExplorationStateIdMappingJob exhibits this error in production when run; we need to debug it:

```
Traceback (most recent call last): (/third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py:508)
  File "/third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py", line 494, in handle
    tstate.input_reader, shard_state, tstate, ctx)
  File "/third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py", line 583, in _process_inputs
    entity, input_reader, ctx, tstate):
  File "/third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py", line 634, in _process_datum
    output_writer.write(output)
  File "/third_party/gae-mapreduce-1.9.17.0/mapreduce/output_writers.py", line 1145, in write
    if len(data) != 2:
TypeError: object of type 'exceptions.Exception' has no len()
```

/cc @BenHenning 


Discussion:
@seanlip I think the error is being raised by the state id map generating functions because of some issue with commit_cmds of some exploration. If commit_cmds are not properly synchronized with exploration versions then it will create the problem. I can't find any other reason for it to happen. 
Chatted with @BenHenning.

- Noted that the `exploration` variable on line 365 is shadowed by the one on line 394, maybe that's relevant.
- @BenHenning suggests removing the yields and reintroducing them one at a time until the error presents, since this is something to do with the stuff yielded by map().
Hi, @seanlip if the current release can go ahead without this Job then feel free to do so. This job can go in next month, too, since I won't be working further on this part until after February.

Also, I don't think that error is because of stuff yielded from map() because all the yields are tuple objects and they have len() == 2. I think something is raising the exception in the map() function. I think that we should cover the body of the for loop (the part where functions generate state id map model for given exploration version) in try ... except block. I think those functions are generating exception.
Hi @prasanna08 -- thanks. We'll drop this job from the current release, then.

And what you suggest sounds fine -- feel free to make changes when you get a chance, and we can re-run in production! Thanks.
@prasanna08 any updates on this?
Hi @jacobdavis11, I was busy with my exams. I will fix this soon. Thanks
Hi @seanlip, last time this job raised Exception when running in production and we were not able to idenfity exact reason for it. I am also not sure why it is raising Exception. If I remember correctly @pranavsid98's stats job also had some issues in production. So how were those issues resolved? Maybe that will give us more idea about this Exception as well. Thanks.
Hm, I think you'll need to ask @pranavsid98. There were a bunch of issues, but I'm not sure how many of them relate to this PR. Also, I think @pranavsid98 is writing up a doc with his findings from the last study (about the issues with prod data) that might be helpful here, so that's another good reason to talk to him!
@prasanna08 I think I remember this issue. Are you yielding an Exception directly? If you are, it has to be something like this - yield(str(Exception)). 
@pranavsid98 that was happening before but then I fixed that issue. This error occurs after fixing that issue. I think this is happening because one of the explorations in production data does not have valid change_dict or something which would cause exception when generating state_id_map fo that exploration. So are there any illegal explorations in production (as in explorations that do not follow the rules they must follow, maybe because they were created before these rules?). Anyway, I think we can find out the cause by wrapping [this](https://github.com/oppia/oppia/blob/develop/core/domain/exp_jobs_one_off.py#L405-L412) portion of code in `try... except...` block and handle exceptions. However, I can't be sure until the job is run in production once again.
@prasanna08 A possible cause is line 365 in exp_jobs_one_off. When you execute get_explration_from_model, the complete schema migration is done, and there are a few explorations in prod which fails the schema migration. I handled it by adding a try-block around the retrieve command. The explorations that show these migration errors are usually the ones with redundant interactions and the like. During stats migration, we skipped such explorations.
@pranavsid98 I see. If that is the case then I will also do the same. Thanks.

Also @seanlip, by any chance would it be possible to see the message of the Exception raised in the production? This will be helpful in tracing the exception.
@prasanna08 how is this going?
@jacobdavis11 the job has been fixed in recent PR (#4755) but we are yet to test it on test server. @seanlip when can we trial run this job on test server?
Whenever you like :) Send me an email giving me steps on what to do, and I'll do it (since I'm coordinating the next release).

Thanks!
Also as I recall, wasn't this issue found when testing with Oppia production data rather than just on the test server?
I dont remember that much. But I have added try and except block at all
critical places. I have high confidence that job would not fail but state
id mapping models for some troublesome exploration might not get generated.

On Mon, Apr 2, 2018, 10:48 PM Ben Henning <notifications@github.com> wrote:

> Also as I recall, wasn't this issue found when testing with Oppia
> production data rather than just on the test server?
>
> —
> You are receiving this because you were mentioned.
>
>
> Reply to this email directly, view it on GitHub
> <https://github.com/oppia/oppia/issues/4190#issuecomment-377983433>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AIRr5U7D9bBeQuWL9ufFEQ4PwzB0Gj9qks5tkl1egaJpZM4Q9Fez>
> .
>
-- 

Best,
Prasanna

> I dont remember that much. But I have added try and except block at all
critical places. I have high confidence that job would not fail but state
id mapping models for some troublesome exploration might not get generated.

In that case, should we add an audit job to verify that all mappings are created as expected?
**UPDATE:** After rerunning the job and fixing a few issues in #4858 and #4854 we have following errors (on test server):
```
[u'ERROR with exp_id 8cbPtcWpWa7m version 3', [u'']]
[u'ERROR with exp_id 9fGjwYXQPxAi version 3', [u'']]
[u'ERROR with exp_id B5_LA7ilD9Zv version 4', [u'']]
[u'ERROR with exp_id Et4VOBCqOatr version 3', [u'']]
[u'ERROR with exp_id R8DZOxupQzds version 3', [u'']]
[u'ERROR with exp_id q4dEZ6fR_1_H version 3', [u'']]
[u'ERROR with exp_id xB0jh1ciQLTH version 3', [u'']]
[u'ERROR with exp_id y2VLOltDeFla version 8', [u'']]
[u'ERROR with exp_id yFoMBd91mIcV version 14', [u'']]
[u'ERROR with exp_id zFNv8PSHhUvE version 35', [u"'END'"]]
```

I am trying to identify source of error.
**UPDATE**
Stack trace of the issues arised in various explorations.

```
* [u'ERROR with exp_id 8cbPtcWpWa7m version 3', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id 9fGjwYXQPxAi version 3', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id B5_LA7ilD9Zv version 4', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id Et4VOBCqOatr version 3', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id R8DZOxupQzds version 3', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id q4dEZ6fR_1_H version 3', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id xB0jh1ciQLTH version 3', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id y2VLOltDeFla version 8', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id yFoMBd91mIcV version 14', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4143, in create_mapping_for_new_version\n assert state_name not in new_state_names_to_ids.keys()\nAssertionError\n']]
* [u'ERROR with exp_id zFNv8PSHhUvE version 35', [u'Traceback (most recent call last):\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_jobs_one_off.py", line 425, in map\n exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_services.py", line 2020, in generate_state_id_mapping_model\n old_exploration, exploration, change_list))\n File "/base/data/home/apps/s~oppiatestserver/2-6-3-staging.408887650733275446/core/domain/exp_domain.py", line 4127, in create_mapping_for_new_version\n new_state = new_exploration.states[state_name]\nKeyError: \'END\'\n']]

```

Resolving patch:
diff --git a/core/domain/exp_domain.py b/core/domain/exp_domain.py
--- a/core/domain/exp_domain.py
+++ b/core/domain/exp_domain.py
@@ -4090,25 +4090,7 @@ def create_mapping_for_new_version(
         # Analyse each command in change list one by one to create state id
         # mapping for new exploration.
         for change_dict in change_list:
-            # During v1 -> v2 migration of states, all pseudo END states were
-            # replaced by an explicit END state through this migration.
-            # We account for that change in the state id mapping too.
-            if change_dict['cmd'] == 'migrate_states_schema_to_latest_version':
-                pseudo_end_state_name = 'END'
-                if int(change_dict['from_version']) < 2 <= int(
-                        change_dict['to_version']):
-                    # The explicit end state is created only if there is some
-                    # state that used to refer to an implicit 'END' state.
-                    # This is confirmed by checking that there is a state
-                    # called 'END' in the immediate version of the exploration
-                    # after migration and no state called 'END' is present
-                    # in previous version of exploration.
-                    if (pseudo_end_state_name in (
-                            new_exploration.states) and (
-                                pseudo_end_state_name not in (
-                                    self.state_names_to_ids))):
-                        new_state_names.append(pseudo_end_state_name)
-            elif change_dict['cmd'] == CMD_ADD_STATE:
+            if change_dict['cmd'] == CMD_ADD_STATE:
                 new_state_names.append(change_dict['state_name'])
                 assert change_dict['state_name'] not in (
                     state_ids_to_names.values())
@@ -4164,6 +4146,30 @@ def create_mapping_for_new_version(
         # with its corresponding old state to find whether significant
         # change has happened or not.
         for (state_id, state_name) in state_ids_to_names.iteritems():
+            if state_name == 'END' and state_name not in new_exploration.states:
+                # If previous version of exploration has END state but new
+                # version does not have END state then exception will be raised
+                # below when comparing old state and corresponding new state.
+                # The exception is raised because there is no commit in change
+                # list corresponding to removal or renaming of END state.
+
+                # This problem arises when following scenario occurrs:
+                # Consider an exploration with two versions a and b (a < b).
+                # Both of these versions have their states dict schema version
+                # set to less than 2.
+                # Now version 'a' has explicit references to END
+                # state and thus when its states dict schema version will be
+                # upgraded to latest version, an 'END' state will be added.
+                # These explicit END references are not present in version 'b',
+                # however, and so no END state will be added during states dict
+                # schema migration.
+
+                # Thus we have no command of END state's removal or renaming
+                # in change_list and there is no END state present in later
+                # version of exploration.
+                new_state_names_to_ids.pop(state_name)
+                continue
+
             new_state = new_exploration.states[state_name]
             old_state = old_exploration.states[old_state_ids_to_names[state_id]]
 
@@ -4173,11 +4179,23 @@ def create_mapping_for_new_version(
                 new_state_names_to_ids.pop(state_name)
                 new_state_names.append(state_name)
 
+        # This may happen in exactly opposite scenario as the one written in
+        # comment above. Previous version of exploration may not have any rule
+        # having END state as its destination and hence during states schema
+        # migration END state won't be added. But next version of exploration
+        # might have END references and, hence, END state might be added
+        # to exploration during states schema migration.
+        # So we check whether such siatuation exists or not. If it exists then
+        # we consider END as a new state to which id will be assigned.
+        if 'END' in new_exploration.states and (
+                'END' not in new_state_names_to_ids and (
+                    'END' not in new_state_names)):
+            new_state_names.append('END')
+
         # Assign new ids to each state in new state names list.
         largest_state_id_used = self.largest_state_id_used
         for state_name in sorted(new_state_names):
             largest_state_id_used += 1
-
             # Check that the state name being assigned is not present in
             # mapping.
             assert state_name not in new_state_names_to_ids.keys()
@@ -4186,6 +4204,23 @@ def create_mapping_for_new_version(
         state_id_map = StateIdMapping(
             new_exploration.id, new_exploration.version, new_state_names_to_ids,
             largest_state_id_used)
+
+        # Do one final check that all state names in new exploration are present
+        # in generated state names to id mapping.
+        if set(new_state_names_to_ids.keys()) != set(
+                new_exploration.states.keys()):
+            unknown_state_names = ((
+                set(new_exploration.states.keys()) -
+                set(new_state_names_to_ids.keys())) + (
+                    set(new_state_names_to_ids.keys()) -
+                    set(new_exploration.states.keys())))
+            raise Exception(
+                'State names to ids mapping does not contain '
+                'state names (%s) which are present in corresponding '
+                'exploration %s, version %d' % (
+                    unknown_state_names,
+                    new_exploration.id, new_exploration.version))
+
         state_id_map.validate()
         return state_id_map
 
@@ -4220,10 +4255,16 @@ def validate(self):
         if len(set(state_ids)) != len(state_ids):
             raise Exception('Assigned state ids should be unique.')
 
-        if not all(state_ids) <= self.largest_state_id_used:
+        if not all(x <= self.largest_state_id_used for x in state_ids):
             raise Exception(
                 'Assigned state ids should be smaller than last state id used.')
 
+        if not all(isinstance(x, int) for x in state_ids):
+            raise Exception(
+                'Assigned state ids should be integer values in '
+                'exploration %s, version %d' % (
+                    self.exploration_id, self.exploration_version))
+
     def get_state_id(self, state_name):
         """Get state id for given state name.
 


Test patch:
diff --git a/core/domain/exp_domain_test.py b/core/domain/exp_domain_test.py
--- a/core/domain/exp_domain_test.py
+++ b/core/domain/exp_domain_test.py
@@ -3265,6 +3265,106 @@ class StateIdMappingTests(test_utils.GenericTestBase):
 
     EXP_ID = 'eid'
 
+    EXPLORATION_CONTENT_1 = ("""default_skin: conversation_v1
+param_changes: []
+param_specs: {}
+schema_version: 1
+states:
+- content:
+  - type: text
+    value: ''
+  name: (untitled state)
+  param_changes: []
+  widget:
+    customization_args: {}
+    handlers:
+    - name: submit
+      rule_specs:
+      - definition:
+          inputs:
+            x: InputString
+          name: Equals
+          rule_type: atomic
+        dest: END
+        feedback:
+          - Correct!
+        param_changes: []
+      - definition:
+          rule_type: default
+        dest: (untitled state)
+        feedback: []
+        param_changes: []
+    sticky: false
+    widget_id: TextInput
+- content:
+  - type: text
+    value: ''
+  name: New state
+  param_changes: []
+  widget:
+    customization_args: {}
+    handlers:
+    - name: submit
+      rule_specs:
+      - definition:
+          rule_type: default
+        dest: END
+        feedback: []
+        param_changes: []
+    sticky: false
+    widget_id: TextInput
+""")
+
+    EXPLORATION_CONTENT_2 = ("""default_skin: conversation_v1
+param_changes: []
+param_specs: {}
+schema_version: 1
+states:
+- content:
+  - type: text
+    value: ''
+  name: (untitled state)
+  param_changes: []
+  widget:
+    customization_args: {}
+    handlers:
+    - name: submit
+      rule_specs:
+      - definition:
+          inputs:
+            x: InputString
+          name: Equals
+          rule_type: atomic
+        dest: New state
+        feedback:
+          - Correct!
+        param_changes: []
+      - definition:
+          rule_type: default
+        dest: (untitled state)
+        feedback: []
+        param_changes: []
+    sticky: false
+    widget_id: TextInput
+- content:
+  - type: text
+    value: ''
+  name: New state
+  param_changes: []
+  widget:
+    customization_args: {}
+    handlers:
+    - name: submit
+      rule_specs:
+      - definition:
+          rule_type: default
+        dest: New state
+        feedback: []
+        param_changes: []
+    sticky: false
+    widget_id: TextInput
+""")
+
     def setUp(self):
         """Initialize owner and store default exploration before each
         test case.
@@ -3314,7 +3414,7 @@ def test_that_mapping_remains_same_when_exp_params_changes(self):
         }
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 0)
 
     def test_that_mapping_is_correct_when_new_state_is_added(self):
@@ -3338,7 +3438,7 @@ def test_that_mapping_is_correct_when_new_state_is_added(self):
         }
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 1)
 
     def test_that_mapping_is_correct_when_old_state_is_deleted(self):
@@ -3366,7 +3466,7 @@ def test_that_mapping_is_correct_when_old_state_is_deleted(self):
         }
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 1)
 
     def test_that_mapping_remains_when_state_is_renamed(self):
@@ -3396,7 +3496,7 @@ def test_that_mapping_remains_when_state_is_renamed(self):
         }
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 1)
 
     def test_that_mapping_is_changed_when_interaction_id_is_changed(self):
@@ -3422,7 +3522,7 @@ def test_that_mapping_is_changed_when_interaction_id_is_changed(self):
 
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 1)
 
     def test_that_mapping_is_correct_for_series_of_changes(self):
@@ -3473,7 +3573,7 @@ def test_that_mapping_is_correct_for_series_of_changes(self):
         }
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 3)
 
         with self.swap(feconf, 'ENABLE_STATE_ID_MAPPING', True):
@@ -3516,5 +3616,119 @@ def test_that_mapping_is_correct_for_series_of_changes(self):
         }
         self.assertEqual(
             new_mapping.exploration_version, new_exploration.version)
-        self.assertEqual(new_mapping.state_names_to_ids, expected_mapping)
+        self.assertDictEqual(new_mapping.state_names_to_ids, expected_mapping)
         self.assertEqual(new_mapping.largest_state_id_used, 5)
+
+    def test_correct_mapping_is_generated_for_exp_with_old_states_schema(self):
+        """Test that correct state id mapping is generated for explorations
+        having old states schema version.
+        """
+
+        # Make sure that END is present in generated state id mapping if
+        # exploration contains rules which have END as their destination state
+        # but exploration itself does not have END state.
+        exploration = exp_domain.Exploration.from_untitled_yaml(
+            self.EXP_ID, 'Title', 'Category', self.EXPLORATION_CONTENT_1)
+        expected_mapping = {
+            '(untitled state)': 0,
+            'New state': 1,
+            'END': 2
+        }
+        state_id_map = (
+            exp_domain.StateIdMapping.create_mapping_for_new_exploration(
+                exploration))
+        self.assertDictEqual(state_id_map.state_names_to_ids, expected_mapping)
+        self.assertEqual(state_id_map.largest_state_id_used, 2)
+
+    def test_mapping_for_exp_with_no_end_reference(self):
+        """Test that correct mapping is generated when old exploration has
+        END state references but new exploration does not have END
+        references.
+        """
+
+        old_exploration = exp_domain.Exploration.from_untitled_yaml(
+            self.EXP_ID, 'Title', 'Category', self.EXPLORATION_CONTENT_1)
+        state_id_map = (
+            exp_domain.StateIdMapping.create_mapping_for_new_exploration(
+                old_exploration))
+
+        # Make sure that END is not present in generated state id mapping, even
+        # though END state may be present in state id mapping of previous
+        # version, if exploration does not contain any rule which has END
+        # as its destination state.
+        new_exploration = exp_domain.Exploration.from_untitled_yaml(
+            self.EXP_ID, 'Title', 'Category', self.EXPLORATION_CONTENT_2)
+        expected_mapping = {
+            '(untitled state)': 0,
+            'New state': 1
+        }
+        state_id_map = state_id_map.create_mapping_for_new_version(
+            old_exploration, new_exploration, [])
+        self.assertDictEqual(
+            state_id_map.state_names_to_ids, expected_mapping)
+        self.assertEqual(state_id_map.largest_state_id_used, 2)
+
+    def test_mapping_for_exploration_with_end_references(self):
+        """Test that correct mapping is generated when old exploration does not
+        have END state reference but new exploration does.
+        """
+
+        old_exploration = exp_domain.Exploration.from_untitled_yaml(
+            self.EXP_ID, 'Title', 'Category', self.EXPLORATION_CONTENT_2)
+        state_id_map = (
+            exp_domain.StateIdMapping.create_mapping_for_new_exploration(
+                old_exploration))
+
+        # Make sure that END is not present in generated state id mapping, even
+        # though END state may be present in state id mapping of previous
+        # version, if exploration does not contain any rule which has END
+        # as its destination state.
+        new_exploration = exp_domain.Exploration.from_untitled_yaml(
+            self.EXP_ID, 'Title', 'Category', self.EXPLORATION_CONTENT_1)
+        expected_mapping = {
+            '(untitled state)': 0,
+            'New state': 1,
+            'END': 2
+        }
+        state_id_map = state_id_map.create_mapping_for_new_version(
+            old_exploration, new_exploration, [])
+        self.assertDictEqual(
+            state_id_map.state_names_to_ids, expected_mapping)
+        self.assertEqual(state_id_map.largest_state_id_used, 2)
+
+    def test_validation(self):
+        """Test validation checks for state id mapping domain object."""
+
+        state_names_to_ids = {
+            'first': 0,
+            'second': 0
+        }
+        largest_state_id_used = 0
+        state_id_mapping = exp_domain.StateIdMapping(
+            'exp_id', 0, state_names_to_ids, largest_state_id_used)
+        with self.assertRaisesRegexp(
+            Exception, 'Assigned state ids should be unique.'):
+            state_id_mapping.validate()
+
+        state_names_to_ids = {
+            'first': 0,
+            'second': 1
+        }
+        largest_state_id_used = 0
+        state_id_mapping = exp_domain.StateIdMapping(
+            'exp_id', 0, state_names_to_ids, largest_state_id_used)
+        with self.assertRaisesRegexp(
+            Exception,
+            'Assigned state ids should be smaller than last state id used.'):
+            state_id_mapping.validate()
+
+        state_names_to_ids = {
+            'first': 0,
+            'second': None
+        }
+        largest_state_id_used = 0
+        state_id_mapping = exp_domain.StateIdMapping(
+            'exp_id', 0, state_names_to_ids, largest_state_id_used)
+        with self.assertRaisesRegexp(
+            Exception, 'Assigned state ids should be integer values'):
+            state_id_mapping.validate()
diff --git a/core/domain/exp_services_test.py b/core/domain/exp_services_test.py
--- a/core/domain/exp_services_test.py
+++ b/core/domain/exp_services_test.py
@@ -3180,7 +3180,6 @@ def test_that_state_id_mapping_model_is_deleted(self):
             exp_services.get_state_id_mapping(
                 exploration.id, exploration.version - 1)
 
-
     def test_that_mapping_is_correct_when_exploration_is_reverted(self):
         """Test that state id mapping is correct when exploration is reverted
         to old version.