Skip to content

Commit

Permalink
Add JSON variable file support (#4542)
Browse files Browse the repository at this point in the history
Implements #4532. Documentation still missing.
  • Loading branch information
sunday2 committed Mar 10, 2023
1 parent 9ffeee3 commit b9b8720
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 2 deletions.
72 changes: 72 additions & 0 deletions atest/robot/variables/json_variable_file.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
*** Settings ***
Suite Setup Run Tests --variablefile ${VARDIR}/cli.json -V ${VARDIR}/cli2.json --pythonpath ${VARDIR}
... variables/json_variable_file.robot
Resource atest_resource.robot

*** Variables ***
${VARDIR} ${DATADIR}/../testresources/res_and_var_files

*** Test Cases ***
Valid JSON file
Check Test Case ${TESTNAME}

Valid JSON file with uper case extension
Check Test Case ${TESTNAME}

Non-ASCII strings
Check Test Case ${TESTNAME}

Dictionary is dot-accessible
Check Test Case ${TESTNAME}

Nested dictionary is dot-accessible
Check Test Case ${TESTNAME}

Dictionary inside list is dot-accessible
Check Test Case ${TESTNAME}

JSON file in PYTHONPATH
Check Test Case ${TESTNAME}

Import Variables keyword
Check Test Case ${TESTNAME}

JSON file from CLI
Check Test Case ${TESTNAME}

Invalid JSON file
Processing should have failed 0 4 invalid.json
... ${EMPTY}
... JSONDecodeError*

Non-mapping JSON file
Processing should have failed 1 5 non_dict.json
... ${EMPTY}
... JSON variable file must be a mapping, got list.

JSON files do not accept arguments
Processing should have failed 2 6 valid.json
... with arguments ? arguments | not | accepted ?${SPACE}
... JSON variable files do not accept arguments.

Non-existing JSON file
Importing should have failed 3 7
... Variable file 'non_existing.Json' does not exist.

JSON with invalid encoding
Processing should have failed 4 8 invalid_encoding.json
... ${EMPTY}
... UnicodeDecodeError*

*** Keywords ***
Processing should have failed
[Arguments] ${index} ${lineno} ${file} ${arguments} ${error}
${path} = Normalize Path ${DATADIR}/variables/${file}
Importing should have failed ${index} ${lineno}
... Processing variable file '${path}' ${arguments}failed:
... ${error}

Importing should have failed
[Arguments] ${index} ${lineno} @{error}
Error In File ${index} variables/json_variable_file.robot ${lineno}
... @{error}
1 change: 1 addition & 0 deletions atest/testdata/variables/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: "jack"
5 changes: 5 additions & 0 deletions atest/testdata/variables/invalid_encoding.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"encoding": "latin-1",
"expected": "utf-8",
"non-ascii": "hyvää yötä!"
}
63 changes: 63 additions & 0 deletions atest/testdata/variables/json_variable_file.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
*** Settings ***
Variables valid.json
Variables pythonpath.json
Variables ./invalid.json
Variables ..${/}variables${/}non_dict.json
Variables valid.json arguments not accepted
Variables non_existing.Json
Variables invalid_encoding.json
Variables valid3.JSON
Test Template Should Be Equal

*** Variables ***
@{EXPECTED LIST} one ${2}
&{EXPECTED DICT} a=1 b=${2} 3=${EXPECTED LIST} key with spaces=value with spaces


*** Test Cases ***
Valid JSON file
${STRING} Hello, YAML!
${INTEGER} ${42}
${FLOAT} ${3.14}
${LIST} ${EXPECTED LIST}
${DICT} ${EXPECTED DICT}
${BOOL} ${TRUE}
${NULL} ${NULL}

Valid JSON file with uper case extension
${STRING IN JSON} Hello, YAML!
${INTEGER IN JSON} ${42}
${FLOAT IN JSON} ${3.14}
${LIST IN JSON} ${EXPECTED LIST}
${DICT IN JSON} ${EXPECTED DICT}
${BOOL IN JSON} ${TRUE}
${NULL IN JSON} ${NULL}

Non-ASCII strings
${NON} äscii
${NÖN} äscii

Dictionary is dot-accessible
${DICT.a} 1
${DICT.b} ${2}

Nested dictionary is dot-accessible
${NESTED DICT.dict} ${EXPECTED DICT}
${NESTED DICT.dict.a} 1
${NESTED DICT.dict.b} ${2}

Dictionary inside list is dot-accessible
${LIST WITH DICT[1].key} value
${LIST WITH DICT[2].dict} ${EXPECTED DICT}
${LIST WITH DICT[2].nested[0].leaf} value

JSON file in PYTHONPATH
${JSON FILE IN PYTHONPATH} ${TRUE}

Import Variables keyword
[Setup] Import Variables ${CURDIR}/valid2.json
${VALID 2} imported successfully

JSON file from CLI
${JSON FILE FROM CLI} woot!
${JSON FILE FROM CLI2} kewl!
6 changes: 6 additions & 0 deletions atest/testdata/variables/non_dict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
"Not dictionary",
{
"true": "top-level"
}
]
55 changes: 55 additions & 0 deletions atest/testdata/variables/valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"string": "Hello, YAML!",
"non": "äscii",
"nön": "äscii",
"integer": 42,
"float": 3.14,
"bool": true,
"null": null,
"list": [
"one",
2
],
"dict": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
},
"nested dict": {
"dict": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
}
},
"list with dict": [
"scalar",
{
"key": "value"
},
{
"dict": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
},
"nested": [
{
"leaf": "value"
}
]
}
]
}
3 changes: 3 additions & 0 deletions atest/testdata/variables/valid2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"valid 2": "imported successfully"
}
20 changes: 20 additions & 0 deletions atest/testdata/variables/valid3.JSON
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"string in JSON": "Hello, YAML!",
"integer in JSON": 42,
"float in JSON": 3.14,
"bool in JSON": true,
"null in JSON": null,
"list in JSON": [
"one",
2
],
"dict in JSON": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
}
}
3 changes: 3 additions & 0 deletions atest/testresources/res_and_var_files/cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"json file from cli": "woot!"
}
3 changes: 3 additions & 0 deletions atest/testresources/res_and_var_files/cli2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"json file from cli2": "kewl!"
}
3 changes: 3 additions & 0 deletions atest/testresources/res_and_var_files/pythonpath.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"json file in pythonpath": true
}
2 changes: 1 addition & 1 deletion src/robot/running/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
class Namespace:
_default_libraries = ('BuiltIn', 'Reserved', 'Easter')
_library_import_by_path_ends = ('.py', '/', os.sep)
_variables_import_by_path_ends = _library_import_by_path_ends + ('.yaml', '.yml')
_variables_import_by_path_ends = _library_import_by_path_ends + ('.yaml', '.yml') + ('.json',)

def __init__(self, variables, suite, resource, languages):
LOGGER.info(f"Initializing namespace for suite '{suite.longname}'.")
Expand Down
27 changes: 27 additions & 0 deletions src/robot/variables/filesetter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import inspect
import io
import json
try:
import yaml
except ImportError:
Expand Down Expand Up @@ -43,6 +44,8 @@ def _import_if_needed(self, path_or_variables, args=None):
% (path_or_variables, args))
if path_or_variables.lower().endswith(('.yaml', '.yml')):
importer = YamlImporter()
elif path_or_variables.lower().endswith('.json'):
importer = JsonImporter()
else:
importer = PythonImporter()
try:
Expand Down Expand Up @@ -147,3 +150,27 @@ def _validate(self, name, value):
if name[0] == '&' and not is_dict_like(value):
raise DataError("Invalid variable '%s': Expected dict-like value, "
"got %s." % (name, type_name(value)))


class JsonImporter:
def import_variables(self, path, args=None):
if args:
raise DataError('JSON variable files do not accept arguments.')
variables = self._import(path)
return [('${%s}' % name, self._dot_dict(value))
for name, value in variables]

def _import(self, path):
with io.open(path, encoding='UTF-8') as stream:
variables = json.load(stream)
if not is_dict_like(variables):
raise DataError('JSON variable file must be a mapping, got %s.'
% type_name(variables))
return variables.items()

def _dot_dict(self, value):
if is_dict_like(value):
return DotDict((k, self._dot_dict(v)) for k, v in value.items())
if is_list_like(value):
return [self._dot_dict(v) for v in value]
return value
2 changes: 1 addition & 1 deletion src/robot/variables/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def as_dict(self, decoration=True):


class GlobalVariables(Variables):
_import_by_path_ends = ('.py', '/', os.sep, '.yaml', '.yml')
_import_by_path_ends = ('.py', '/', os.sep, '.yaml', '.yml', '.json')

def __init__(self, settings):
super().__init__()
Expand Down

0 comments on commit b9b8720

Please sign in to comment.