#103 Make the list of allowed outcomes configurable.
Closed 6 years ago by jskladan. Opened 6 years ago by ralph.
taskotron/ ralph/resultsdb configurable-outcomes-on-develop  into  develop

@@ -0,0 +1,30 @@ 

+ """Change outcome from enum to string.

+ 

+ Revision ID: cd581d0e83df

+ Revises: 4dbe714897fe

+ Create Date: 2018-03-28 20:47:27.338605

+ 

+ """

+ 

+ # revision identifiers, used by Alembic.

+ revision = 'cd581d0e83df'

+ down_revision = '4dbe714897fe'

+ branch_labels = None

+ depends_on = None

+ 

+ from alembic import op

+ import sqlalchemy as sa

+ 

+ 

+ def upgrade():

+     op.execute(

+         "ALTER TABLE result "

+         "ALTER COLUMN outcome "

+         "TYPE VARCHAR(32)")

+ 

+ 

+ def downgrade():

+     op.execute(

+         "ALTER TABLE result "

+         "ALTER COLUMN outcome "

+         "TYPE resultoutcome USING outcome::resultoutcome")

file modified
+4
@@ -46,6 +46,9 @@ 

          'create_testcase': [],

          }

  

+     # Extend the list of allowed outcomes.

+     ADDITIONAL_RESULT_OUTCOMES = ()

+ 

      # Supported values: "oidc"

      AUTH_MODULE = None

  
@@ -110,3 +113,4 @@ 

      TRAP_BAD_REQUEST_ERRORS = True

      FEDMENU_URL = 'https://apps.stg.fedoraproject.org/fedmenu'

      FEDMENU_DATA_URL = 'https://apps.stg.fedoraproject.org/js/data.js'

+     ADDITIONAL_RESULT_OUTCOMES = ('AMAZING',)

@@ -683,5 +683,6 @@ 

                      "documentation": "http://docs.resultsdb.apiary.io/",

                      "jobs": url_for('.get_jobs', _external=True),

                      "results": url_for('.get_results', _external=True),

-                     "testcases": url_for('.get_testcases', _external=True)

+                     "testcases": url_for('.get_testcases', _external=True),

+                     "outcomes": RESULT_OUTCOME,

                      }), 300

@@ -779,5 +779,6 @@ 

                      "documentation": "http://docs.resultsdb20.apiary.io/",

                      "groups": url_for('.get_groups', _external=True),

                      "results": url_for('.get_results', _external=True),

-                     "testcases": url_for('.get_testcases', _external=True)

+                     "testcases": url_for('.get_testcases', _external=True),

+                     "outcomes": RESULT_OUTCOME,

                      }), 300

file modified
+4 -4
@@ -20,14 +20,14 @@ 

  import datetime

  import uuid as lib_uuid

  

- from resultsdb import db

+ from resultsdb import db, app

  from resultsdb.serializers import DBSerialize

  

  

  __all__ = ['Testcase', 'Group', 'Result', 'ResultData', 'GroupsToResults', 'RESULT_OUTCOME']

  

- 

- RESULT_OUTCOME = ('PASSED', 'INFO', 'FAILED', 'NEEDS_INSPECTION')

+ PRESET_OUTCOMES = ('PASSED', 'INFO', 'FAILED', 'NEEDS_INSPECTION')

+ RESULT_OUTCOME = PRESET_OUTCOMES + app.config.get('ADDITIONAL_RESULT_OUTCOMES', [])

  JOB_STATUS = []

  

  
@@ -95,7 +95,7 @@ 

      testcase_name = db.Column(db.Text, db.ForeignKey('testcase.name'))

  

      submit_time = db.Column(db.DateTime, default=datetime.datetime.utcnow)

-     outcome = db.Column(db.Enum(*RESULT_OUTCOME, name='resultoutcome'))

+     outcome = db.Column(db.String(32))

      note = db.Column(db.Text)

      ref_url = db.Column(db.Text)

  

@@ -853,3 +853,27 @@ 

          assert plugin.history[0]['groups'] == [self.ref_group_uuid]

          assert plugin.history[0]['note'] == self.ref_result_note

          assert plugin.history[0]['testcase']['name'] == self.ref_testcase_name

+ 

+     def test_create_result_custom_outcome(self):

+         self.test_create_group()

+         self.test_create_testcase()

+         ref_result = copy.deepcopy(self.ref_result)

+         ref_result['outcome'] = 'AMAZING'

+ 

+         r, data = self.helper_create_result(outcome='AMAZING')

+ 

+         assert r.status_code == 201

+         assert data == ref_result

+ 

+         ref_result = copy.deepcopy(self.ref_result)

+         ref_result['outcome'] = 'SILLY'

+ 

+         r, data = self.helper_create_result(outcome='SILLY')

+ 

+         assert r.status_code == 400

+ 

+     def test_get_outcomes_on_landing_page(self):

+         r = self.app.get('/api/v2.0/')

+         data = json.loads(r.data)

+         assert r.status_code == 300

+         assert data['outcomes'] == ['PASSED', 'INFO', 'FAILED', 'NEEDS_INSPECTION', 'AMAZING']

This will let other resultsdb instances expand the list.

How about the following so that user's don't have to change their config?

RESULT_OUTCOME = app.config.get('RESULT_OUTCOME', ('PASSED', 'INFO', 'FAILED', 'NEEDS_INSPECTION'))

Then you could remove the entry in config.py above.

rebased onto 876a4aa8d2592c6eca01726db937a5cc67ef0b6b

6 years ago

Wouldn't this cause issues with DBs where the list of values is internally implemented as a set, when "what appears to be configuration" gets changed, but really, also a DB migration is needed?

I don't have any huge issues with the concept, but there are some nitpicks. One is in the previous comment. The other being, that at the moment, the list of allowed values for the OUTCOME is static, and you can expect to have the same available with any resultsbd you encounter.
While I understand that the list is by all means not exhaustive, and I agree that a simpler way of introducing "other values" is a good thing (tm), I'd much rather see this adding new configurable values to the set of already existing ones, instead of plain replacing the default set.
I also think, that with the set of "allowed" values now being dynamic, it would be nice to have that list at least semi-discoverable - it is certainly not the best solution, but adding a line with something like "allowed_outcomes" to the response on the / route would IMO be nice.
Do you think this is reasonable, or do you believe that just plain-replacing the outcome-set is a better solution to the problem? Also, what is the problem? :D

Wouldn't this cause issues with DBs where the list of values is internally implemented as a set, when "what appears to be configuration" gets changed, but really, also a DB migration is needed?

Quite possibly. Where would you expect to hit this? postgres?

I'd much rather see this adding new configurable values to the set of already existing ones, instead of plain replacing the default set.

Super idea. Will do.

...you [could previously] expect to have the same available with any resultsbd you encounter.

Good point. Only adding to the list instead of replacing it will help with this, but not resolve it. :(

it would be nice to have that list at least semi-discoverable

Yeah, agreed. Will add. :)

Also, what is the problem? :D

Heh, I've got two. The first is, I have some stakeholders that call for a NOT_APPLICABLE outcome as a requirement. I suggested using INFO for this, but they want to use both for different purposes. The second is the problem that execdb solves in the taskotron world. People really want to be able to see "pending" or "scheduled" when a result is absent.. and for good reason!

Quite possibly. Where would you expect to hit this? postgres?

Yeah, Postgres is one that I know of (and we use it...) I'm not sure how to do this in a sane way :( I guess people will have to read the docs, but we all know how that goes :D
Another solution could be changing the enum type to string (I'd rather see that limited to a reasonable amount of characters, even though this is just a gut feeling and not really based on any kind of facts), and thus eliminating the need for the DB migration. Heck, this is probably the solution, the more I think about it.
I'm of course very open to more informed reasoning :)

Good point. Only adding to the list instead of replacing it will help with this, but not resolve it. :(

That is true, but IMO having a "standard set plus extras" is a bit better than "random different sets" (exaggerated but I think you know what I mean). Not ideal, but has less of the stink, and I can't think of a better solution anyway. There still could be an argument made for just not specifying the outcome set at all, but I feel that a reasonable default is better than free-for-all.

Yeah, agreed. Will add. :)
Awesome, thanks!

Heh, I've got two. The first is, I have some stakeholders that call for a NOT_APPLICABLE outcome as a requirement. I suggested using INFO for this, but they want to use both for different purposes. The second is the problem that execdb solves in the taskotron world. People really want to be able to see "pending" or "scheduled" when a result is absent.. and for good reason!

Yeah, I can see that. We wanted to clearly separate those two, and I like that cleanliness of responsibility-separation, but I can understand that sometimes the beauty must make small step back for practicality.
And the design goal for resultsdb was "do whatever, this is a glorified key-value store anyway", so even though I would not be willing to do this in Taskotron, I don't see why this particular piece of code should be an obstacle in somebody else's happiness :D

Thanks @jskladan!

I'll add the db migration to convert from an enum to a string in here too (along with some of the other changes). It may not show up for another day or so, though. Will ping when it's ready for review again.

rebased onto 6a3315d

6 years ago

1 new commit added

  • Add outcomes to landing page, for discoverability.
6 years ago

1 new commit added

  • Replace the enum outcome with a string.
6 years ago

OK - should be ready for review again.

One thing I was unsure about - do you want the @validates validator for the outcome on the ORM level? There's plenty of validation already in api_v1.py and api_v2.py.

3 new commits added

  • Replace the enum outcome with a string.
  • Add outcomes to landing page, for discoverability.
  • Make the list of allowed outcomes configurable.
6 years ago

This is IMO not really needed. As you said in the comment, the validation is already done on the controller front,.
If there is a reason for keeping it, that I'm not seeing, maybe rename it to validate_outcome :)

Awesome, could you also add something like:

    def test_create_result_custom_outcome(self):
        self.test_create_group()
        self.test_create_testcase()
        ref_result = copy.deepcopy(self.ref_result)
        ref_result['outcome'] = 'AMAZING'

        r, data = self.helper_create_result(outcome='AMAZING')

        assert r.status_code == 201
        assert data == ref_result

Thanks!

Apart of the nitpicks above, this looks good to me!

2 new commits added

  • Test creation of results with custom outcomes.
  • Remove unnecessary validator.
6 years ago

Cool! I'll test it and merge tomorrow. Thanks for bearing with me! :)

Pull-Request has been closed by jskladan

6 years ago