Compare commits

...

8 Commits
0_3 ... master

Author SHA1 Message Date
Tim Bentley a324e3c87c Merge branch 'updates' into 'master'
Updates to version 0.4

See merge request openlp/openlp_api_tester!9
2022-02-19 08:18:50 +00:00
Tim 674ae581ce
Add Active tag and zeroconv handler. 2022-02-19 08:15:40 +00:00
Tim a5be688755
Merge branch 'updates' of gitlab.com:trb143/openlp_api_tester into updates 2022-02-12 22:04:20 +00:00
Tim 53db7e744a
Cleanup and add defaults 2022-02-12 18:00:27 +00:00
Tim 2a897edb10
Cleanup and add defaults 2022-02-12 17:02:13 +00:00
Tim 73b9a511f0
Cleanup and add defaults 2022-02-12 17:02:09 +00:00
Tim Bentley 87a05a947c Merge branch 'jan2021' into 'master'
Jan2021

See merge request openlp/openlp_api_tester!8
2021-08-21 15:16:29 +00:00
Tim Bentley 5acab38871 Jan2021 2021-08-21 15:16:29 +00:00
32 changed files with 575 additions and 288 deletions

View File

@ -9,4 +9,4 @@ lint-yaml:
image: python:latest
script:
- pip install yamllint
- yamllint *.yaml
- yamllint scripts/*.yaml

View File

@ -1,14 +1,18 @@
# OpenLP API tester.
A commandline utility to access a running OpenLP instance via it's API's and test all functionality.
A command line utility to access a running OpenLP instance via it's API's and test all functionality.
It will find OpenLP on the network via ZeroConf and then using the API's build and run a service.
This harness will find a running OpenLP instance on the network via ZeroConf and then using the API's build and run a service.
After each command the results are tested to make sure the WebSockets have changed.
The API's need to be configured as insecure as this is not being tested.
Tests are mastered in JSON files and run using the framework.
## Test Structure
The bible test framework is random so some bible references will not be correct and therefore will not load! The chapter and verse numbers are random numbers!!
## Sample Test Structure
```
process_name: test process
step1:
@ -38,29 +42,43 @@ step6:
The step number, needs to be unique and in order!
* delay
* delay (optional)
How long to wait after a call has been made. Default is 2 seconds for all calls.
How long to wait after a call has been made. Default is 0.3 seconds for all calls.
* max
* max (optional)
How long to wait after a call has been made. Default is 2 seconds for all calls.
Max number of fields which will change on the Web Sockets interface.
* min
* min (optional)
How long to wait after a call has been made. Default is 2 seconds for all calls.
Max number of fields which will change on the Web Sockets interface.
* payload
How long to wait after a call has been made. Default is 2 seconds for all calls.
Json to support addition information to the call.
* name
The step name (function name) to be called to run the step.
## Commands
## Commands and Payloads
| Rule Name | Payload |
|--------------------------|------------------------------|
| trigger_alert | text: <Some text to display> |
| search_and_add | plugin: <plugin name>|
| search_and_live | plugin: <plugin name>|
| service_item_show | item: item_id|
| play_live_item | ?????|
| controller_item_next |?????|
| controller_item_previous |???????|
## Adding API's and Internals
API's and their handers are defining in the callback.py file. This should be the only file that needs to change when adding new API's
The test scripts are in the scripts directory and are a pre defined set of tests.

View File

@ -1,13 +0,0 @@
---
process_name: test process
step1:
max: 1
min: 1
name: clear_live_controller
step2:
delay: 1
name: clear_preview_controller
step3:
max: 1
min: 1
name: new_service

View File

@ -1,13 +0,0 @@
---
process_name: test process
step1:
max: 1
min: 1
name: clear_live_controller
step2:
delay: 1
name: clear_preview_controller
step3:
max: 1
min: 1
name: new_service

View File

@ -1,85 +0,0 @@
---
process_name: test process
step1:
max: 1
min: 1
name: clear_live_controller
step2:
delay: 1
name: clear_preview_controller
step3:
max: 1
min: 1
name: new_service
step4:
delay: 1
name: search_and_add
payload:
plugin: songs
step5:
delay: 1
name: search_and_add
payload:
plugin: songs
step6:
delay: 1
name: search_and_add
payload:
plugin: bibles
step7:
delay: 1
name: search_and_add
payload:
plugin: bibles
step8:
delay: 1
name: search_and_add
payload:
plugin: custom
step9:
delay: 1
name: search_and_add
payload:
plugin: images
step10:
delay: 1
name: search_and_add
payload:
plugin: songs
step11:
delay: 1
name: search_and_add
payload:
plugin: presentation
step12:
delay: 1
name: search_and_add
payload:
plugin: bibles
step13:
delay: 1
name: search_and_add
payload:
plugin: bibles
step14:
delay: 1
name: search_and_add
payload:
plugin: songs
step15:
delay: 1
name: search_and_add
payload:
plugin: custom
step16:
delay: 1
name: search_and_add
payload:
plugin: bibles
step17:
delay: 1
name: search_and_add
payload:
plugin: bibles
step18:
name: load_service_random

View File

@ -1,80 +0,0 @@
---
process_name: test process
step1:
max: 1
min: 1
name: clear_live_controller
step2:
delay: 1
name: clear_preview_controller
step3:
max: 1
min: 1
name: new_service
step4:
delay: 1
name: search_and_add
payload:
plugin: songs
step5:
delay: 1
name: search_and_add
payload:
plugin: songs
step6:
delay: 1
name: search_and_add
payload:
plugin: bibles
step7:
delay: 1
name: search_and_add
payload:
plugin: bibles
step8:
delay: 1
name: search_and_add
payload:
plugin: custom
step9:
delay: 1
name: search_and_add
payload:
plugin: images
step10:
delay: 1
name: search_and_add
payload:
plugin: songs
step12:
delay: 1
name: search_and_add
payload:
plugin: bibles
step13:
delay: 1
name: search_and_add
payload:
plugin: bibles
step14:
delay: 1
name: search_and_add
payload:
plugin: songs
step15:
delay: 1
name: search_and_add
payload:
plugin: custom
step16:
delay: 1
name: search_and_add
payload:
plugin: bibles
step17:
delay: 1
name: search_and_add
payload:
plugin: bibles
step18:
name: load_service_sequential

View File

@ -1,4 +0,0 @@
---
process_name: live_item
step1:
name: play_live_items

View File

@ -1,21 +0,0 @@
---
process_name: play media
step1:
max: 1
min: 1
name: clear_live_controller
step2:
delay: 1
name: clear_preview_controller
step3:
delay: 10
name: search_and_add
payload:
plugin: media
step4:
name: load_service_sequential
step5:
delay: 30
name: flush_pending
step6:
name: media_pause

View File

@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}

View File

@ -1,4 +0,0 @@
---
process_name: Pause Media
step1:
name: media_pause

View File

@ -1,4 +0,0 @@
---
process_name: Play Media
step1:
name: media_play

View File

@ -1,3 +1,4 @@
python3-colorama==0.4.3
websocket-client==0.56.0
websocket-client~=1.2.3
zeroconf~=0.36.9
colorama~=0.4.4
requests~=2.27.0

13
scripts/alert.json Normal file
View File

@ -0,0 +1,13 @@
{
"process_name": "Show basic alert process",
"step1": {"active": true,
"name": "trigger_alert",
"min": 0,
"payload": { "text": "Some Text" }
},
"step2": {"active": true,
"name": "trigger_alert",
"delay": 5,
"payload": { "text": "Some more text Text" }
}
}

35
scripts/blank.json Normal file
View File

@ -0,0 +1,35 @@
{
"process_name": "Test Display hiding",
"step1": {"active": true,
"name": "display_hide",
"delay": 1
},
"step2": {"active": true,
"name": "display_show",
"delay": 1
},
"step3": {"active": true,
"name": "display_blank",
"delay": 1
},
"step4": {"active": true,
"name": "display_show",
"delay": 1
},
"step5": {"active": true,
"name": "display_theme",
"delay": 1
},
"step6": {"active": true,
"name": "display_show",
"delay": 1
},
"step7": {"active": true,
"name": "display_desktop",
"delay": 1
},
"step8": {"active": true,
"name": "display_show",
"delay": 1
}
}

104
scripts/full_run_rand.json Normal file
View File

@ -0,0 +1,104 @@
{
"process_name": "Test Process Service Random",
"step1": {"active": true,
"name": "clear_live_controller",
"max": 1,
"min": 1
},
"step2": {"active": true,
"name": "clear_preview_controller",
"delay": 1
},
"step3": {"active": true,
"name": "new_service",
"max": 1,
"min": 1
},
"step4": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step5": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step6": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step7": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {"active": true,
"plugin": "custom"
}
},
"step8": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "images"
}
},
"step9": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step10": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step11": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step12": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step13": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "custom"
}
},
"step14": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step15": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step16": {"active": true,
"name": "load_service_random"
}
}

104
scripts/full_run_seq.json Normal file
View File

@ -0,0 +1,104 @@
{
"process_name": "Test Process Service Sequential",
"step1": {"active": true,
"name": "clear_live_controller",
"max": 1,
"min": 1
},
"step2": {"active": true,
"name": "clear_preview_controller",
"delay": 1
},
"step3": {"active": true,
"name": "new_service",
"max": 1,
"min": 1
},
"step4": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step5": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step6": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step7": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "custom"
}
},
"step8": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "images"
}
},
"step9": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step10": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step11": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step12": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step13": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "custom"
}
},
"step14": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "bibles"
}
},
"step15": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step16": {"active": true,
"name": "load_service_sequential"
}
}

6
scripts/live_item.json Normal file
View File

@ -0,0 +1,6 @@
{
"process_name": "live_item",
"step1": {"active": true,
"name": "play_live_items"
}
}

29
scripts/media.json Normal file
View File

@ -0,0 +1,29 @@
{
"process_name": "play media",
"step1": {"active": true,
"name": "clear_live_controller",
"max": 1,
"min": 1
},
"step2": {"active": true,
"name": "clear_preview_controller",
"delay": 1
},
"step3": {"active": true,
"name": "search_and_add",
"delay": 10,
"payload": {
"plugin": "media"
}
},
"step4": {"active": true,
"name": "load_service_sequential"
},
"step5": {"active": true,
"name": "flush_pending",
"delay": 30
},
"step6": {"active": true,
"name": "media_pause"
}
}

6
scripts/pause.json Normal file
View File

@ -0,0 +1,6 @@
{
"process_name": "Pause Media",
"step1": {"active": true,
"name": "media_pause",
}
}

6
scripts/play.json Normal file
View File

@ -0,0 +1,6 @@
{
"process_name": "Play Media",
"step1": {"active": true,
"name": "media_play",
}
}

6
scripts/stop.json Normal file
View File

@ -0,0 +1,6 @@
{
"process_name": "Stop Media",
"step1": {"active": true,
"name": "media_stop",
}
}

32
scripts/test.json Normal file
View File

@ -0,0 +1,32 @@
{
"process_name": "play media",
"step1": {"active": true,
"name": "clear_live_controller",
"max": 1,
"min": 1
},
"step2": {"active": true,
"name": "clear_preview_controller",
"delay": 1
},
"step3": {"active": true,
"name": "new_service",
"max": 1,
"min": 1
},
"step4": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {
"plugin": "songs"
}
},
"step5": {"active": true,
"name": "search_and_add",
"delay": 1,
"payload": {"plugin": "bibles" }
},
"step6": {"active": true,
"name": "load_service_sequential"
}
}

15
scripts/themes.json Normal file
View File

@ -0,0 +1,15 @@
{
"process_name": "Themes",
"step1": {"active": true,
"name": "set_theme_level",
"payload": { "theme_level": "global" }
},
"step2": {"active": true,
"delay": 2,
"name": "change_default_theme"
},
"step3": {"active": true,
"delay": 2,
"name": "change_default_theme"
}
}

View File

@ -1,4 +0,0 @@
---
process_name: Stop Media
step1:
name: media_stop

View File

@ -1,25 +0,0 @@
---
process_name: test process
step1:
max: 1
min: 1
name: clear_live_controller
step2:
delay: 1
name: clear_preview_controller
step3:
max: 1
min: 1
name: new_service
step4:
delay: 1
name: search_and_add
payload:
plugin: songs
step5:
delay: 1
name: search_and_add
payload:
plugin: bibles
step6:
name: load_service_sequential

View File

@ -167,7 +167,7 @@ def play_live_items(rtc: object) -> None:
ret = requests.get(rtc.base_url + 'controller/live-items')
assert ret.status_code == 200, f'{ret.status_code} returned from live_item'
i = 0
for _ in json.loads(ret.text):
for _ in json.loads(ret.text)['slides']:
pl = {'max': 1, 'min': 2, 'delay': 0.5, 'name': 'controller_item_show', 'payload': {'id': i}}
rtc.tasks.append(Task(rtc, pl))
i += 1
@ -187,3 +187,139 @@ def controller_item_show(rtc: object, payload: dict) -> None:
print_text('Controller_item_show')
ret = requests.post(rtc.base_url + 'controller/show', json=dict(id=int(id)))
assert ret.status_code == 204, ret.status_code
def controller_item_next(rtc: object, payload: dict) -> None:
print_text('Controller_item_next')
ret = requests.post(rtc.base_url + 'controller/progress', json=dict(action='next'))
assert ret.status_code == 204, ret.status_code
def controller_item_previous(rtc: object, payload: dict) -> None:
print_text('Controller_item_previous')
ret = requests.post(rtc.base_url + 'controller/progress', json=dict(action='previous'))
assert ret.status_code == 204, ret.status_code
def trigger_alert(rtc: object, payload: dict) -> None:
p_text = payload['text']
print_text('trigger_alert')
ret = requests.post(rtc.base_url + 'plugins/alerts', json=dict(text=p_text))
assert ret.status_code == 204, ret.status_code
def get_plugins(rtc: object) -> None:
print_text('get_plugins')
ret = requests.get(rtc.base_url + 'plugins')
assert ret.status_code == 204, ret.status_code
def get_system(rtc: object) -> None:
print_text('get_system')
ret = requests.get(rtc.base_url + 'system')
assert ret.status_code == 204, ret.status_code
def get_live_image(rtc: object) -> None:
print_text('get_live_image')
ret = requests.get(rtc.base_url + 'live-image')
assert ret.status_code == 204, ret.status_code
def change_default_theme(rtc: object) -> None:
print_text('loop_theme_default')
items = get_themes(rtc)
default = get_default_theme(rtc)
first = None
for item in json.loads(items):
if item['name'].replace("\"", "") != default and first is None:
first = item['name']
set_theme(rtc, first)
# items = requests.get(rtc.base_url + 'service/items')
# service = json.loads(items.text)
# limit = len(service)
# random_service = [random.randint(1, limit) for itr in range(limit)]
# # test sequentially
# for item in random_service:
# pl = {'max': 1, 'min': 1, 'name': 'service_item_show', 'payload': {'item': item}}
# rtc.pending.append(Task(rtc, pl))
def get_themes(rtc: object) -> list:
print_text('get_themes')
ret = requests.get(rtc.base_url + 'controller/themes')
assert ret.status_code == 200, ret.status_code
return ret.text
def get_default_theme(rtc: object) -> str:
print_text('get_default_theme')
ret = requests.get(rtc.base_url + 'controller/theme')
assert ret.status_code == 200, ret.status_code
return ret.text.replace("\"", "").replace("\n", "")
def get_themes_name(rtc: object, payload: dict) -> None:
t_name = payload['theme_name']
print_text('get_themes_name')
ret = requests.get(rtc.base_url + f'themes/{t_name}')
assert ret.status_code == 204, ret.status_code
return ret.text
def get_live_theme(rtc: object) -> None:
print_text('get_live_theme')
ret = requests.get(rtc.base_url + 'live_theme')
assert ret.status_code == 204, ret.status_code
def get_theme_level(rtc: object) -> None:
print_text('get_theme_level')
ret = requests.get(rtc.base_url + 'controller/theme')
assert ret.status_code == 200, ret.status_code
return ret.text
def set_theme_level(rtc: object, payload: dict) -> None:
t_level = payload['theme_level']
print_text('set_theme_level')
ret = requests.post(rtc.base_url + 'controller/theme-level', json=dict(level=t_level))
assert ret.status_code == 204, ret.status_code
return ret.text
def set_theme(rtc: object, theme_name: str) -> None:
print_text('set_theme')
ret = requests.post(rtc.base_url + 'controller/theme', json=dict(theme=theme_name))
assert ret.status_code == 204, ret.status_code
def display_hide(rtc: object) -> None:
print_text('display_hide')
ret = requests.post(rtc.base_url + 'core/display', json=dict(display='hide'))
assert ret.status_code == 204, ret.status_code
def display_show(rtc: object) -> None:
print_text('display_show')
ret = requests.post(rtc.base_url + 'core/display', json=dict(display='show'))
assert ret.status_code == 204, ret.status_code
def display_blank(rtc: object) -> None:
print_text('display_blank')
ret = requests.post(rtc.base_url + 'core/display', json=dict(display='blank'))
assert ret.status_code == 204, ret.status_code
def display_theme(rtc: object) -> None:
print_text('display_theme')
ret = requests.post(rtc.base_url + 'core/display', json=dict(display='theme'))
assert ret.status_code == 204, ret.status_code
def display_desktop(rtc: object) -> None:
print_text('display_desktop')
ret = requests.post(rtc.base_url + 'core/display', json=dict(display='desktop'))
assert ret.status_code == 204, ret.status_code

View File

@ -23,24 +23,24 @@ init(autoreset=True)
def print_text(text: str):
print(Fore.LIGHTMAGENTA_EX + '[*] = ' + text)
print(Fore.LIGHTMAGENTA_EX + f'[*] {text}')
def print_error(text: str):
print(Fore.RED + '[-] = ' + text)
print(Fore.RED + f'[-] {text}')
def print_ok(text: str):
print(Fore.GREEN + '[_] = ' + text)
print(Fore.GREEN + f'[_] {text}')
def print_warn(text: str):
print(Fore.YELLOW + '[?] = ' + text)
print(Fore.YELLOW + f'[?] {text}')
def print_info(text: str):
print(Fore.MAGENTA + '[!] = ' + text)
print(Fore.MAGENTA + f'[!] {text}')
def print_debug(text: str):
print(Fore.CYAN + '[#] = ' + text)
print(Fore.CYAN + f'[#] {text}')

View File

@ -20,7 +20,7 @@
import argparse
import threading
import websocket
import yaml
import json
from collections import deque
from websocket import create_connection
@ -69,7 +69,7 @@ class RunTestsController(object):
if not conn_timeout:
print_error("Could not connect to WS! Exiting.")
def on_message(self, message: str) -> None:
def on_message(self, ws_obj: object, message: str) -> None:
if self.debug:
print_debug(f"Message returned: {message}")
if not self.received:
@ -78,15 +78,15 @@ class RunTestsController(object):
self.ws_data.append(message)
@staticmethod
def on_open() -> None:
print_info("Socket Listener Opened")
def on_open(ws_obj: object) -> None:
print_info(f"Socket Listener Opened")
@staticmethod
def on_close() -> None:
def on_close(ws_obj: object) -> None:
print_info("Socket Listener Closed")
@staticmethod
def on_error(error: str) -> None:
def on_error(ws_obj: object, error: str) -> None:
print_error(f'WebSocket Error: {error}')
def load_and_check_sockets(self, first_run: bool = False) -> bool:
@ -116,13 +116,15 @@ class RunTestsController(object):
parser = argparse.ArgumentParser()
parser.add_argument('rargs', nargs='*', default=[])
a = parser.parse_args()
with open(a.rargs[0], 'r') as file:
commands = yaml.load(file, Loader=yaml.FullLoader)
f_open = f'{a.rargs[0]}'
with open(f_open, 'r') as file:
commands = json.load(file)
for step in commands:
if step == 'process_name':
print_info(f'Processing file {commands[step]}')
continue
self.tasks.append(Task(self, commands[step]))
if commands[step]['active'] is True:
self.tasks.append(Task(self, commands[step]))
self.process_queue()
def compare_stage(self):

View File

@ -23,6 +23,11 @@ from test_api.apitest.callbacks import * # noqa E403
def human_delay(delay: int = 2) -> None:
"""
Delay time in Seconds
:param delay:
:return:
"""
time.sleep(delay)

View File

@ -18,7 +18,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import socket
from zeroconf import ServiceBrowser, Zeroconf
from zeroconf import ServiceBrowser, Zeroconf, ServiceInfo
from time import sleep
from test_api.apitest.logger import print_info
@ -38,9 +38,14 @@ def check_for_openlp(type: str) -> str:
info = zeroconf.get_service_info(type, name)
print_info(f'Service {name} added, service info: {info}')
if info.name.startswith("OpenLP"):
print_info(f'Service {name} found')
self.port = str(info.port)
self.address = socket.inet_ntoa(info.addresses[0])
@staticmethod
def update_service(info: ServiceInfo) -> None:
pass
zeroconf = Zeroconf()
my_listener = MyListener()
_ = ServiceBrowser(zeroconf, f'_{type}._tcp.local.', my_listener)

View File

@ -27,15 +27,18 @@ def start() -> None:
Instantiate and run the tests.
:return:
"""
print_text('OpenLP - API Test Runner V0_2')
print_text('OpenLP - API Test Runner V0_3')
print_text('Check OpenLP is running')
op_address, op_http_port = check_for_openlp('http')
if not op_http_port:
print_error('OpenLP is not running ')
return
print_error('OpenLP is not found - defaulting to 4316')
op_http_port = 4316
else:
print_ok(f'OpenLP is running on port (http) {op_http_port}')
_, op_ws_port = check_for_openlp('ws')
if not op_ws_port:
print_error('OpenLP is not found - defaulting to 4317')
op_ws_port = 4317
print_ok(f'OpenLP is running network Address {op_address}')
print_ok(f'OpenLP is running on port (ws) {op_ws_port}')
rtc = RunTestsController(op_address, op_http_port, op_ws_port)

7
test_api/temp.py Normal file
View File

@ -0,0 +1,7 @@
import json
with open('/home/tim/Projects/OpenLP/openlp_api_tester/scripts/alert.yaml', 'r') as file:
commands = json.load(file) #, Loader=yaml.BaseLoader)
print(commands)
for step in commands:
print(step)