Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration Tests for Custom Jinja Filters in Mistral #3565

Merged
merged 35 commits into from
Jul 31, 2017

Conversation

Mierdin
Copy link
Member

@Mierdin Mierdin commented Jul 12, 2017

This PR contains integration tests for custom Jinja filters made available in Mistral via StackStorm/st2mistral#30. Also tests for changes to st2kv in same PR (addition of decrypt parameter). Note that these tests will not pass until that PR is merged (or if you check it out locally).

All tests ensure compatibility with both YAQL and Jinja rendering within Mistral.

Test Output

These tests haven't been running in CI lately due to some other issues, so here's some output showing the tests passing:

(virtualenv)vagrant@st2dev:~/st2$ nosetests --rednose --immediate -s -v st2tests/integration/mistral/test_filters.py

test_json_escape (integration.mistral.test_filters.JsonEscapeFiltersTest) ... passed
test_regex_match (integration.mistral.test_filters.RegexMatchFiltersTest) ... passed
test_regex_nomatch (integration.mistral.test_filters.RegexMatchFiltersTest) ... passed
test_regex_replace (integration.mistral.test_filters.RegexReplaceFiltersTest) ... passed
test_regex_nosearch (integration.mistral.test_filters.RegexSearchFiltersTest) ... passed
test_regex_search (integration.mistral.test_filters.RegexSearchFiltersTest) ... passed
test_regex_substring (integration.mistral.test_filters.RegexSubstringFiltersTest) ... passed
test_to_complex (integration.mistral.test_filters.ToComplexFiltersTest) ... passed
test_to_human_time_from_seconds (integration.mistral.test_filters.ToHumanTimeFromSecondsFiltersTest) ... passed
test_to_json_string (integration.mistral.test_filters.ToJsonStringFiltersTest) ... passed
test_to_yaml_string (integration.mistral.test_filters.ToYamlStringFiltersTest) ... passed
test_use_none (integration.mistral.test_filters.UseNoneFiltersTest) ... passed
test_version_bump_major (integration.mistral.test_filters.VersionBumpMajorFiltersTest) ... passed
test_version_bump_minor (integration.mistral.test_filters.VersionBumpMinorFiltersTest) ... passed
test_version_bump_patch (integration.mistral.test_filters.VersionBumpPatchFiltersTest) ... passed
test_version_compare (integration.mistral.test_filters.VersionCompareFiltersTest) ... passed
test_version_equal (integration.mistral.test_filters.VersionEqualFiltersTest) ... passed
test_version_less_than (integration.mistral.test_filters.VersionLessThanFiltersTest) ... passed
test_version_match (integration.mistral.test_filters.VersionMatchFiltersTest) ... passed
test_version_more_than (integration.mistral.test_filters.VersionMoreThanFiltersTest) ... passed
test_version_strip_patch (integration.mistral.test_filters.VersionStripPatchFiltersTest) ... passed





TEST RESULT OUTPUT:

-----------------------------------------------------------------------------
21 tests run in 169.939 seconds (21 tests passed)
(virtualenv)vagrant@st2dev:~/st2$ nosetests --rednose --immediate -s -v st2tests/integration/mistral/test_st2kv.py
test_jinja_system_kvp (integration.mistral.test_st2kv.EncryptedKeyValuePairTest) ... passed
test_jinja_user_kvp (integration.mistral.test_st2kv.EncryptedKeyValuePairTest) ... passed
test_yaql_system_kvp (integration.mistral.test_st2kv.EncryptedKeyValuePairTest) ... passed
test_yaql_user_kvp (integration.mistral.test_st2kv.EncryptedKeyValuePairTest) ... passed
test_jinja_system_kvp (integration.mistral.test_st2kv.UnencryptedKeyValuePairTest) ... passed
test_jinja_user_kvp (integration.mistral.test_st2kv.UnencryptedKeyValuePairTest) ... passed
test_yaql_system_kvp (integration.mistral.test_st2kv.UnencryptedKeyValuePairTest) ... passed
test_yaql_user_kvp (integration.mistral.test_st2kv.UnencryptedKeyValuePairTest) ... passed





TEST RESULT OUTPUT:

-----------------------------------------------------------------------------
8 tests run in 31.336 seconds (8 tests passed)

Matt Oswalt added 13 commits July 26, 2017 18:47
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
Signed-off-by: Matt Oswalt <[email protected]>
@Mierdin Mierdin added the RFR label Jul 27, 2017
@Mierdin Mierdin changed the title [WIP] Integration Tests for Custom Jinja Filters in Mistral Integration Tests for Custom Jinja Filters in Mistral Jul 27, 2017
@Mierdin Mierdin requested a review from Kami July 27, 2017 14:34
Copy link
Contributor

@m4dcoder m4dcoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. Need to rework a few test workflows so it's more readable in the tests. Need to rename folder and workflows. I prefer we use the term "function" instead of "filter" in general. The term "filter" is specific to Jinja. In Mistral, the equivalent is called "function". I will be more strict in the use of this term in the patch at st2mistral.

action: std.noop
publish:
result_jinja: '[{"title": "{{ _.input_str|json_escape }}"}]'
result_yaql: '[{"title": "<% json_escape($.input_str) %>"}]'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix carriage return here?

Copy link
Member Author

@Mierdin Mierdin Jul 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 13a2d0d (as well as a few other missing newlines as well as some extra ones)

---
description: Example for using the custom filter "json_escape"
enabled: true
entry_point: workflows/customfilters/mistral-json_escape.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the naming of the workflow either with all dashes or all underscores but not mixing them in the name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename the customfilters folder to tests? And explicitly name mistral-json_escape.yaml to something like mistral-test-filter-json-escape.yaml? So end up entry_point: workflows/tests/mistral-test-filter-json-escape.yaml. I may put all other test related workflows into this folder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c51dc60

task1:
action: std.noop
publish:
result_jinja: '{{ _.input_str | regex_match(_.regex_pattern)}}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to pipe _.input_str? You can't pass it directly into regex_match?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just going with the style that's commonly seen in workflows. No technical reason one way or another

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then please be consistent between the YAQL and Jinja expression and use regex_match(input_str, pattern), unless you're testing whether values can be piped into the function (which may not be a bad idea but do it separately).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will change the Jinja usage to look like YAQL

Copy link
Member Author

@Mierdin Mierdin Jul 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was wrong, there is a technical reason to use this notation (at least the way the underlying function is implemented right now of course). Using these functions in Jinja in the same way that they're used in YAQL will pass context as the first argument. So when I made this change, I got a lot of these types of errors:

JinjaEvaluationException: Can not evaluate Jinja expression [expression= version_strip_patch(_.version) , error=version_strip_patch() takes exactly 1 argument (2 given), data={}]

None of these filters actually require direct access to context, only the value passed in, so while it would be possible to modify the underlying implementation to access context, I'm leaning towards leaving things the way they are because 1) least-privilege principles and 2) the filter format (|) is the generally expected way this is done. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling the filter as normal function call is the minimal requirement IMO. Being able to use the filter from piping is added bonus. Because right now, it's an exception and we will have to document and explain to people the only way to use the function in Jinja is using this format. I also need to know exactly why it can't be used like normal function calls.

Copy link
Member Author

@Mierdin Mierdin Jul 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing. So, I threw together a quick filter to simply print out the positional args that are being passed into it:

def test_filter(*args):
    return str(args)

Then put together a workflow for testing (ignore the regex naming, I just used that as a testbed):

version: '2.0'

examples.mistral-customfilters-regex_match:
    description: A workflow for testing regex_match custom filter in mistral
    type: direct
    input:
        - input_str
        - regex_pattern
    output:
        result_jinja: <% $.result_jinja %>
        result_yaql: <% $.result_yaql %>
    tasks:

        task1:
            action: std.noop
            publish:
                result_jinja: '{{ _.input_str | test_filter }}'
                result_yaql: '<% test_filter($.input_str) %>'

When we use the pipe (in Jinja's eyes, filter usage), the first arg is simply the string value being passed in:

(virtualenv)vagrant@st2dev:~/st2$ segl
id: 597abe9232ed355b4e873d3e
action.ref: examples.mistral-customfilters-regex_match
parameters:
  input_str: Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.
  regex_pattern: ([0-9]{3} \w+ (?:Ave|St|Dr))
status: succeeded (5s elapsed)
start_timestamp: 2017-07-28T04:33:22.578088Z
end_timestamp: 2017-07-28T04:33:27.492128Z
result:
  extra:
    state: SUCCESS
    state_info: null
  result_jinja: (u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.',)
  result_yaql: (u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.',)
  tasks:
  - created_at: '2017-07-28 04:33:23'
    id: 213d3c60-b380-49e3-a725-6b76f6f84c0c
    input: null
    name: task1
    published:
      result_jinja: (u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.',)
      result_yaql: (u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.',)
    result: null
    state: SUCCESS
    state_info: null
    updated_at: '2017-07-28 04:33:23'
    workflow_execution_id: 37d5f7b6-5af2-445a-b5fe-d72cd490c1c3
    workflow_name: examples.mistral-customfilters-regex_match

However, if we change the Jinja snippet to behave like a function instead of a filter....

'{{ test_filter(_.input_str) }}'

...then the first parameter will be the context, followed by the string value:

(virtualenv)vagrant@st2dev:~/st2$ segl
id: 597abfc332ed356072d3b2f1
action.ref: examples.mistral-customfilters-regex_match
parameters:
  input_str: Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.
  regex_pattern: ([0-9]{3} \w+ (?:Ave|St|Dr))
status: succeeded (5s elapsed)
start_timestamp: 2017-07-28T04:38:27.931985Z
end_timestamp: 2017-07-28T04:38:32.919549Z
result:
  extra:
    state: SUCCESS
    state_info: null
  result_jinja: ({}, u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.')
  result_yaql: (u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.',)
  tasks:
  - created_at: '2017-07-28 04:38:28'
    id: bd3568b5-ed16-494e-8ca2-c76cb76227db
    input: null
    name: task1
    published:
      result_jinja: ({}, u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.')
      result_yaql: (u'Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave.',)
    result: null
    state: SUCCESS
    state_info: null
    updated_at: '2017-07-28 04:38:28'
    workflow_execution_id: 6dc96bc0-f8e0-47d5-bf74-60c313715e12
    workflow_name: examples.mistral-customfilters-regex_match

The reason this is happening is that when used as a function, Jinja2 injects it's own Context object as the very first parameter. What complicates things is that Mistral is ALSO passing in its own context parameter (the empty dict we see in the previous example) all the time, but the use of partial in https://github.com/StackStorm/mistral/blob/master/mistral/utils/expression_utils.py#L92 means we don't see this. However, if we use this as a function, the Jinja2 Context object becomes args[0], and Mistral's context becomes args[1], leaving value at args[2].

So, TLDR the reason the examples (see the ActionChains in examples pack, for instance - their back-end functions are implemented in the same way) use the pipe notation is because all of the backend functions have the string value as the first parameter. It would seem that the current implementation requires the pipe notation.

Now, this obviously doesn't HAVE to be the case - I could, for instance, create a decorator to handle these variations seamlessly (and/or use one of the decorators at http://jinja.pocoo.org/docs/2.9/api/#utilities), but whatever we do for Jinja in Mistral should get copied over to st2 so we don't undo the work to sync the two "islands".

Hopefully that makes sense. I'll take a stab at trying to make the underlying functions care less about how they're called, keeping in mind that it will have to also be repeated in st2.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also might be worth exploring the idea of modifying Mistral's use of partial to specify a keyword arg instead of positional, to be more precise, but I haven't thought that one through yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I have something working in StackStorm/st2mistral@cb87499. I've tested both forms and it seems to work well.
Let me know what you think about the approach and I'll add the decorator to the rest of the filters in that PR and then add a new dedicated test in this PR for testing both forms on a few of the filters.

task2:
action: std.noop
publish:
result_jinja: '[{"title": "{{ _.input_str|json_escape }}"}]'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to pipe _.input_str? You can't pass it directly into json_escape?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


REGEX_SEARCH_STR = "Your address is 567 Elsewhere Dr. My address is 123 Somewhere Ave."
REGEX_SEARCH_STR_2 = "567 Elsewhere Dr is your address. My address is 123 Somewhere Ave."
REGEX_SEARCH_STR_3 = "No address to be found here! Well, maybe 127.0.0.1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use an array? REGEX_SEARCH_STRINGS = ["Your address...", "567 Elsewhere...", "No address..."]

Copy link
Member Author

@Mierdin Mierdin Jul 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could do that, just did it this way since I wasn't planning on iterating over them, just referring to specific ones. Could put in list and refer by index

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4961fcc


def test_regex_substring(self):
execution = self._execute_workflow('examples.mistral-customfilters-regex_substring',
parameters={"input_str": REGEX_SEARCH_STR})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the regex pattern from vars to input parameters here? It was hard to follow at first why the output of the workflow returns "567 Elsewhere Dr"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good - did that for this and all other regex tests in b3fcf21

inputs = {'input_str': 'foo'}
execution = self._execute_workflow(
'examples.mistral-customfilters-use_none', parameters=inputs
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow is a bit confusing to me. How about split this into two separate tests where the input parameter to the workflow is optional and default to null? The first test passes the "foo" string. The second tests don't pass any inputs and let it default to null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - good catch, I meant to circle back and do this. My first thought was to literally pass None as a parameter but there's no JSON schema type to support it, so I went with this approach so I could just create a new variable in the workflow that's null. I realized after this that an empty string would suffice just as well (duh), just forgot to go back and simplify. Will do that next.

Copy link
Member Author

@Mierdin Mierdin Jul 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, now I remember. The underlying implementation actually explicitly checks for None (I mistakenly thought it was a simple if not value: but it is is literally if value is None) https://github.com/StackStorm/st2mistral/pull/30/files#diff-d0cfa0e285114587d5c640c4937d3d77R26

So to keep from modifying the underlying function (which would technically be a breaking change - or at least a deviation from the sister function in st2), I need to be able to supply null values, and I can't do that if the parameter type is string. Thoughts either way (as in changing this workflow to fit the existing function vs changing the way the function works)?


def test_to_complex(self):
execution = self._execute_workflow(
'examples.mistral-customfilters-to_complex'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the complex object to be pass in as input parameters instead of using examples.object_return. It's more readable for the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - examples.object_return was available and useful, but it's better to be consistent with the way the rest of the tests are written.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9eb20b7

def test_to_json_string(self):
execution = self._execute_workflow(
'examples.mistral-customfilters-to_json_string'
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the json to be pass in as input parameters instead of using examples.object_return. It's more readable for the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9eb20b7


def test_to_yaml_string(self):
execution = self._execute_workflow(
'examples.mistral-customfilters-to_yaml_string'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the json to be pass in as input parameters instead of using examples.object_return. It's more readable for the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9eb20b7

@m4dcoder
Copy link
Contributor

Please see why the st2/packages job is failing of this PR.

@Mierdin
Copy link
Member Author

Mierdin commented Jul 28, 2017

@m4dcoder I believe packages was failing because of the issue @Kami fixed in StackStorm/st2-packages#475. I'll push more commits here anyways - if it's still failing I'll tshoot it then

Matt Oswalt added 2 commits July 30, 2017 21:09
This follows changes being made on the st2mistral side

Signed-off-by: Matt Oswalt <[email protected]>
@Mierdin Mierdin added this to the 2.4.0 milestone Jul 31, 2017
Copy link
Contributor

@m4dcoder m4dcoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than inconsistency in naming of workflow and associated files.

description: Example for using the custom filter "json_escape"
enabled: true
entry_point: workflows/tests/mistral-json-escape.yaml
name: mistral-customfilters-json-escape
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make naming consistent. Usually, I name the workflow and the file name the same. For example, name the workflow mistral-test-func-json-escape, the meta file should be mistral-test-func-json-escape.yaml and the entry point should be workflows/tests/mistral-test-func-json-escape.yaml.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 821ddb2

@Mierdin Mierdin merged commit 7824a09 into master Jul 31, 2017
@Mierdin Mierdin deleted the mistral-jinja-filter-itests branch July 31, 2017 21:02
@cognifloyd cognifloyd removed the RFR label Aug 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants