From ae9ba4afa1044071227a37268700c4acf897f68e Mon Sep 17 00:00:00 2001 From: Leonid Evdokimov Date: Thu, 3 Jul 2014 10:32:31 +0400 Subject: [PATCH] uri: provide raw_content, parse json without double-decoding. Fixes #7586 Regression potential: - `raw_content` is written to `dest` file instead of decoded `content` - `raw_content` doubles module reply --- test/integration/roles/test_uri/files/README | 9 ++ .../roles/test_uri/files/fail0.json | 1 + .../roles/test_uri/files/fail1.json | 1 + .../roles/test_uri/files/fail10.json | 1 + .../roles/test_uri/files/fail11.json | 1 + .../roles/test_uri/files/fail12.json | 1 + .../roles/test_uri/files/fail13.json | 1 + .../roles/test_uri/files/fail14.json | 1 + .../roles/test_uri/files/fail15.json | 1 + .../roles/test_uri/files/fail16.json | 1 + .../roles/test_uri/files/fail17.json | 1 + .../roles/test_uri/files/fail18.json | 1 + .../roles/test_uri/files/fail19.json | 1 + .../roles/test_uri/files/fail2.json | 1 + .../roles/test_uri/files/fail20.json | 1 + .../roles/test_uri/files/fail21.json | 1 + .../roles/test_uri/files/fail22.json | 1 + .../roles/test_uri/files/fail23.json | 1 + .../roles/test_uri/files/fail24.json | 1 + .../roles/test_uri/files/fail25.json | 1 + .../roles/test_uri/files/fail26.json | 2 + .../roles/test_uri/files/fail27.json | 2 + .../roles/test_uri/files/fail28.json | 1 + .../roles/test_uri/files/fail29.json | 1 + .../roles/test_uri/files/fail3.json | 1 + .../roles/test_uri/files/fail30.json | 1 + .../roles/test_uri/files/fail4.json | 1 + .../roles/test_uri/files/fail5.json | 1 + .../roles/test_uri/files/fail6.json | 1 + .../roles/test_uri/files/fail7.json | 1 + .../roles/test_uri/files/fail8.json | 1 + .../roles/test_uri/files/fail9.json | 1 + .../roles/test_uri/files/pass0.json | 58 +++++++++ .../roles/test_uri/files/pass1.json | 1 + .../roles/test_uri/files/pass2.json | 6 + .../roles/test_uri/files/pass3.json | 1 + .../roles/test_uri/files/pass4.json | 1 + .../roles/test_uri/handlers/main.yml | 3 + test/integration/roles/test_uri/meta/main.yml | 2 + .../integration/roles/test_uri/tasks/main.yml | 120 ++++++++++++++++++ 40 files changed, 234 insertions(+) create mode 100644 test/integration/roles/test_uri/files/README create mode 100644 test/integration/roles/test_uri/files/fail0.json create mode 100644 test/integration/roles/test_uri/files/fail1.json create mode 100644 test/integration/roles/test_uri/files/fail10.json create mode 100644 test/integration/roles/test_uri/files/fail11.json create mode 100644 test/integration/roles/test_uri/files/fail12.json create mode 100644 test/integration/roles/test_uri/files/fail13.json create mode 100644 test/integration/roles/test_uri/files/fail14.json create mode 100644 test/integration/roles/test_uri/files/fail15.json create mode 100644 test/integration/roles/test_uri/files/fail16.json create mode 100644 test/integration/roles/test_uri/files/fail17.json create mode 100644 test/integration/roles/test_uri/files/fail18.json create mode 100644 test/integration/roles/test_uri/files/fail19.json create mode 100644 test/integration/roles/test_uri/files/fail2.json create mode 100644 test/integration/roles/test_uri/files/fail20.json create mode 100644 test/integration/roles/test_uri/files/fail21.json create mode 100644 test/integration/roles/test_uri/files/fail22.json create mode 100644 test/integration/roles/test_uri/files/fail23.json create mode 100644 test/integration/roles/test_uri/files/fail24.json create mode 100644 test/integration/roles/test_uri/files/fail25.json create mode 100644 test/integration/roles/test_uri/files/fail26.json create mode 100644 test/integration/roles/test_uri/files/fail27.json create mode 100644 test/integration/roles/test_uri/files/fail28.json create mode 100644 test/integration/roles/test_uri/files/fail29.json create mode 100644 test/integration/roles/test_uri/files/fail3.json create mode 100644 test/integration/roles/test_uri/files/fail30.json create mode 100644 test/integration/roles/test_uri/files/fail4.json create mode 100644 test/integration/roles/test_uri/files/fail5.json create mode 100644 test/integration/roles/test_uri/files/fail6.json create mode 100644 test/integration/roles/test_uri/files/fail7.json create mode 100644 test/integration/roles/test_uri/files/fail8.json create mode 100644 test/integration/roles/test_uri/files/fail9.json create mode 100644 test/integration/roles/test_uri/files/pass0.json create mode 100644 test/integration/roles/test_uri/files/pass1.json create mode 100644 test/integration/roles/test_uri/files/pass2.json create mode 100644 test/integration/roles/test_uri/files/pass3.json create mode 100644 test/integration/roles/test_uri/files/pass4.json create mode 100644 test/integration/roles/test_uri/handlers/main.yml create mode 100644 test/integration/roles/test_uri/meta/main.yml create mode 100644 test/integration/roles/test_uri/tasks/main.yml diff --git a/test/integration/roles/test_uri/files/README b/test/integration/roles/test_uri/files/README new file mode 100644 index 00000000000..ef7791262b4 --- /dev/null +++ b/test/integration/roles/test_uri/files/README @@ -0,0 +1,9 @@ +The files were taken from http://www.json.org/JSON_checker/ +> If the JSON_checker is working correctly, it must accept all of the pass*.json files and reject all of the fail*.json files. + +Difference with JSON_checker dataset: + - *${n}.json renamed to *${n-1}.json to be 0-based + - fail0.json renamed to pass3.json as python json module allows JSON payload to be string + - fail17.json renamed to pass4.json as python json module has no problems with deep structures + - fail32.json renamed to fail0.json to fill gap + - fail31.json renamed to fail17.json to fill gap diff --git a/test/integration/roles/test_uri/files/fail0.json b/test/integration/roles/test_uri/files/fail0.json new file mode 100644 index 00000000000..ca5eb19dc97 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail0.json @@ -0,0 +1 @@ +["mismatch"} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail1.json b/test/integration/roles/test_uri/files/fail1.json new file mode 100644 index 00000000000..6b7c11e5a56 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail1.json @@ -0,0 +1 @@ +["Unclosed array" \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail10.json b/test/integration/roles/test_uri/files/fail10.json new file mode 100644 index 00000000000..76eb95b4583 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail10.json @@ -0,0 +1 @@ +{"Illegal expression": 1 + 2} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail11.json b/test/integration/roles/test_uri/files/fail11.json new file mode 100644 index 00000000000..77580a4522d --- /dev/null +++ b/test/integration/roles/test_uri/files/fail11.json @@ -0,0 +1 @@ +{"Illegal invocation": alert()} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail12.json b/test/integration/roles/test_uri/files/fail12.json new file mode 100644 index 00000000000..379406b59bd --- /dev/null +++ b/test/integration/roles/test_uri/files/fail12.json @@ -0,0 +1 @@ +{"Numbers cannot have leading zeroes": 013} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail13.json b/test/integration/roles/test_uri/files/fail13.json new file mode 100644 index 00000000000..0ed366b38a3 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail13.json @@ -0,0 +1 @@ +{"Numbers cannot be hex": 0x14} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail14.json b/test/integration/roles/test_uri/files/fail14.json new file mode 100644 index 00000000000..fc8376b605d --- /dev/null +++ b/test/integration/roles/test_uri/files/fail14.json @@ -0,0 +1 @@ +["Illegal backslash escape: \x15"] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail15.json b/test/integration/roles/test_uri/files/fail15.json new file mode 100644 index 00000000000..3fe21d4b532 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail15.json @@ -0,0 +1 @@ +[\naked] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail16.json b/test/integration/roles/test_uri/files/fail16.json new file mode 100644 index 00000000000..62b9214aeda --- /dev/null +++ b/test/integration/roles/test_uri/files/fail16.json @@ -0,0 +1 @@ +["Illegal backslash escape: \017"] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail17.json b/test/integration/roles/test_uri/files/fail17.json new file mode 100644 index 00000000000..45cba7396ff --- /dev/null +++ b/test/integration/roles/test_uri/files/fail17.json @@ -0,0 +1 @@ +{"Comma instead if closing brace": true, \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail18.json b/test/integration/roles/test_uri/files/fail18.json new file mode 100644 index 00000000000..3b9c46fa9a2 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail18.json @@ -0,0 +1 @@ +{"Missing colon" null} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail19.json b/test/integration/roles/test_uri/files/fail19.json new file mode 100644 index 00000000000..27c1af3e72e --- /dev/null +++ b/test/integration/roles/test_uri/files/fail19.json @@ -0,0 +1 @@ +{"Double colon":: null} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail2.json b/test/integration/roles/test_uri/files/fail2.json new file mode 100644 index 00000000000..168c81eb785 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail2.json @@ -0,0 +1 @@ +{unquoted_key: "keys must be quoted"} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail20.json b/test/integration/roles/test_uri/files/fail20.json new file mode 100644 index 00000000000..62474573b21 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail20.json @@ -0,0 +1 @@ +{"Comma instead of colon", null} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail21.json b/test/integration/roles/test_uri/files/fail21.json new file mode 100644 index 00000000000..a7752581bcf --- /dev/null +++ b/test/integration/roles/test_uri/files/fail21.json @@ -0,0 +1 @@ +["Colon instead of comma": false] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail22.json b/test/integration/roles/test_uri/files/fail22.json new file mode 100644 index 00000000000..494add1ca19 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail22.json @@ -0,0 +1 @@ +["Bad value", truth] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail23.json b/test/integration/roles/test_uri/files/fail23.json new file mode 100644 index 00000000000..caff239bfc3 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail23.json @@ -0,0 +1 @@ +['single quote'] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail24.json b/test/integration/roles/test_uri/files/fail24.json new file mode 100644 index 00000000000..8b7ad23e010 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail24.json @@ -0,0 +1 @@ +[" tab character in string "] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail25.json b/test/integration/roles/test_uri/files/fail25.json new file mode 100644 index 00000000000..845d26a6a54 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail25.json @@ -0,0 +1 @@ +["tab\ character\ in\ string\ "] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail26.json b/test/integration/roles/test_uri/files/fail26.json new file mode 100644 index 00000000000..6b01a2ca4a9 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail26.json @@ -0,0 +1,2 @@ +["line +break"] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail27.json b/test/integration/roles/test_uri/files/fail27.json new file mode 100644 index 00000000000..621a0101c66 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail27.json @@ -0,0 +1,2 @@ +["line\ +break"] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail28.json b/test/integration/roles/test_uri/files/fail28.json new file mode 100644 index 00000000000..47ec421bb62 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail28.json @@ -0,0 +1 @@ +[0e] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail29.json b/test/integration/roles/test_uri/files/fail29.json new file mode 100644 index 00000000000..8ab0bc4b8b2 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail29.json @@ -0,0 +1 @@ +[0e+] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail3.json b/test/integration/roles/test_uri/files/fail3.json new file mode 100644 index 00000000000..9de168bf34e --- /dev/null +++ b/test/integration/roles/test_uri/files/fail3.json @@ -0,0 +1 @@ +["extra comma",] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail30.json b/test/integration/roles/test_uri/files/fail30.json new file mode 100644 index 00000000000..1cce602b518 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail30.json @@ -0,0 +1 @@ +[0e+-1] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail4.json b/test/integration/roles/test_uri/files/fail4.json new file mode 100644 index 00000000000..ddf3ce3d240 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail4.json @@ -0,0 +1 @@ +["double extra comma",,] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail5.json b/test/integration/roles/test_uri/files/fail5.json new file mode 100644 index 00000000000..ed91580e1b1 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail5.json @@ -0,0 +1 @@ +[ , "<-- missing value"] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail6.json b/test/integration/roles/test_uri/files/fail6.json new file mode 100644 index 00000000000..8a96af3e4ee --- /dev/null +++ b/test/integration/roles/test_uri/files/fail6.json @@ -0,0 +1 @@ +["Comma after the close"], \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail7.json b/test/integration/roles/test_uri/files/fail7.json new file mode 100644 index 00000000000..b28479c6ecb --- /dev/null +++ b/test/integration/roles/test_uri/files/fail7.json @@ -0,0 +1 @@ +["Extra close"]] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail8.json b/test/integration/roles/test_uri/files/fail8.json new file mode 100644 index 00000000000..5815574f363 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail8.json @@ -0,0 +1 @@ +{"Extra comma": true,} \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/fail9.json b/test/integration/roles/test_uri/files/fail9.json new file mode 100644 index 00000000000..5d8c0047bd5 --- /dev/null +++ b/test/integration/roles/test_uri/files/fail9.json @@ -0,0 +1 @@ +{"Extra value after close": true} "misplaced quoted value" \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/pass0.json b/test/integration/roles/test_uri/files/pass0.json new file mode 100644 index 00000000000..70e26854369 --- /dev/null +++ b/test/integration/roles/test_uri/files/pass0.json @@ -0,0 +1,58 @@ +[ + "JSON Test Pattern pass1", + {"object with 1 member":["array with 1 element"]}, + {}, + [], + -42, + true, + false, + null, + { + "integer": 1234567890, + "real": -9876.543210, + "e": 0.123456789e-12, + "E": 1.234567890E+34, + "": 23456789012E66, + "zero": 0, + "one": 1, + "space": " ", + "quote": "\"", + "backslash": "\\", + "controls": "\b\f\n\r\t", + "slash": "/ & \/", + "alpha": "abcdefghijklmnopqrstuvwyz", + "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", + "digit": "0123456789", + "0123456789": "digit", + "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", + "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", + "true": true, + "false": false, + "null": null, + "array":[ ], + "object":{ }, + "address": "50 St. James Street", + "url": "http://www.JSON.org/", + "comment": "// /* */": " ", + " s p a c e d " :[1,2 , 3 + +, + +4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], + "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", + "quotes": "" \u0022 %22 0x22 034 "", + "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" +: "A key can be any string" + }, + 0.5 ,98.6 +, +99.44 +, + +1066, +1e1, +0.1e1, +1e-1, +1e00,2e+00,2e-00 +,"rosebud"] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/pass1.json b/test/integration/roles/test_uri/files/pass1.json new file mode 100644 index 00000000000..d3c63c7ad84 --- /dev/null +++ b/test/integration/roles/test_uri/files/pass1.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/pass2.json b/test/integration/roles/test_uri/files/pass2.json new file mode 100644 index 00000000000..4528d51f1ac --- /dev/null +++ b/test/integration/roles/test_uri/files/pass2.json @@ -0,0 +1,6 @@ +{ + "JSON Test Pattern pass3": { + "The outermost value": "must be an object or array.", + "In this test": "It is an object." + } +} diff --git a/test/integration/roles/test_uri/files/pass3.json b/test/integration/roles/test_uri/files/pass3.json new file mode 100644 index 00000000000..6216b865f10 --- /dev/null +++ b/test/integration/roles/test_uri/files/pass3.json @@ -0,0 +1 @@ +"A JSON payload should be an object or array, not a string." \ No newline at end of file diff --git a/test/integration/roles/test_uri/files/pass4.json b/test/integration/roles/test_uri/files/pass4.json new file mode 100644 index 00000000000..edac92716f1 --- /dev/null +++ b/test/integration/roles/test_uri/files/pass4.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/test/integration/roles/test_uri/handlers/main.yml b/test/integration/roles/test_uri/handlers/main.yml new file mode 100644 index 00000000000..2283208d191 --- /dev/null +++ b/test/integration/roles/test_uri/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: stop SimpleHTTPServer + shell: start-stop-daemon --stop --pidfile {{ output_dir }}/SimpleHTTPServer.pid --exec {{ py2.stdout }} diff --git a/test/integration/roles/test_uri/meta/main.yml b/test/integration/roles/test_uri/meta/main.yml new file mode 100644 index 00000000000..07faa217762 --- /dev/null +++ b/test/integration/roles/test_uri/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_tests diff --git a/test/integration/roles/test_uri/tasks/main.yml b/test/integration/roles/test_uri/tasks/main.yml new file mode 100644 index 00000000000..6dd23df86ca --- /dev/null +++ b/test/integration/roles/test_uri/tasks/main.yml @@ -0,0 +1,120 @@ +# test code for the uri module +# (c) 2014, Leonid Evdokimov + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +- name: set role facts + set_fact: + http_port: 15260 + files_dir: '{{ _original_file|dirname }}/../files' + checkout_dir: '{{ output_dir }}/git' + +- name: verify that python2 is installed so this test can continue + shell: which python2 + register: py2 + +- name: start SimpleHTTPServer + shell: start-stop-daemon --start --pidfile {{ output_dir }}/SimpleHTTPServer.pid --background --make-pidfile --chdir {{ files_dir }} --exec {{ py2.stdout }} -- -m SimpleHTTPServer {{ http_port }} + notify: stop SimpleHTTPServer + +- wait_for: port={{ http_port }} + + +- name: md5 pass_json + stat: path={{ files_dir }}/{{ item }}.json get_md5=yes + register: pass_md5 + with_sequence: start=0 end=4 format=pass%d + +- name: fetch pass_json + uri: return_content=yes url=http://localhost:{{ http_port }}/{{ item }}.json + register: pass + with_sequence: start=0 end=4 format=pass%d + +- name: check pass_json + assert: + that: + - '"json" in item.1' + - item.0.stat.md5 == item.1.raw_content | md5 + with_together: + - pass_md5.results + - pass.results + + +- name: md5 fail_json + stat: path={{ files_dir }}/{{ item }}.json get_md5=yes + register: fail_md5 + with_sequence: start=0 end=30 format=fail%d + +- name: fetch fail_json + uri: return_content=yes url=http://localhost:{{ http_port }}/{{ item }}.json + register: fail + with_sequence: start=0 end=30 format=fail%d + +- name: check fail_json + assert: + that: + - item.0.stat.md5 == item.1.raw_content | md5 + - '"json" not in item.1' + with_together: + - fail_md5.results + - fail.results + + +- name: check content != raw_content + assert: + that: item.content != item.raw_content + with_items: + - '{{ pass.results.0 }}' + - '{{ fail.results.14 }}' + - '{{ fail.results.15 }}' + - '{{ fail.results.16 }}' + - '{{ fail.results.27 }}' + +- name: check content == raw_content + assert: + that: item.content == item.raw_content + with_items: + - '{{ pass.results.1 }}' + - '{{ pass.results.2 }}' + - '{{ pass.results.3 }}' + - '{{ pass.results.4 }}' + - '{{ fail.results.0 }}' + - '{{ fail.results.1 }}' + - '{{ fail.results.2 }}' + - '{{ fail.results.3 }}' + - '{{ fail.results.4 }}' + - '{{ fail.results.5 }}' + - '{{ fail.results.6 }}' + - '{{ fail.results.7 }}' + - '{{ fail.results.8 }}' + - '{{ fail.results.9 }}' + - '{{ fail.results.10 }}' + - '{{ fail.results.11 }}' + - '{{ fail.results.12 }}' + - '{{ fail.results.13 }}' + - '{{ fail.results.17 }}' + - '{{ fail.results.18 }}' + - '{{ fail.results.19 }}' + - '{{ fail.results.20 }}' + - '{{ fail.results.21 }}' + - '{{ fail.results.22 }}' + - '{{ fail.results.23 }}' + - '{{ fail.results.24 }}' + - '{{ fail.results.25 }}' + - '{{ fail.results.26 }}' + - '{{ fail.results.28 }}' + - '{{ fail.results.29 }}' + - '{{ fail.results.30 }}'