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

Support FaaS[Noy fully automated yet] #10

Merged
merged 4 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
branches:
- main

jobs:
Linux:
jobs:
Linux_cli:
name: Linux - Ubuntu Run
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -36,9 +36,9 @@ jobs:

- name: Run Tests Suits
run: |
find test-suites -type f -name "*.yaml" -exec python ./testing.py -f {} -V \;
find test-suites -type f -name "*.yaml" -exec python ./testing.py -f {} -V -e cli \;

Windows:
Windows_cli:
name: Windows Run
runs-on: windows-latest
steps:
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
- name: Run Test Suits
shell: bash
run: |
find test-suites -type f -name "*.yaml" -exec python ./testing.py -f {} -V \;
find test-suites -type f -name "*.yaml" -exec python ./testing.py -f {} -V -e cli \;


# MacOS:
Expand All @@ -100,4 +100,4 @@ jobs:

# - name: Run Tests Suits
# run: |
# find test-suites -type f -name "*.yaml" -exec python ./testing.py -f {} -V \;
# find test-suites -type f -name "*.yaml" -exec python ./testing.py -f {} -V -e cli \;
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ code-files:
- path: random-password-generator-example/app.py
test-cases:
- name: Check the password is generated in the correct length
command: call getRandomPassword(12)
function-call: getRandomPassword(12)
expected-pattern: '\"[\w\W]{12}\"'
- name: Check the password is generated in the correct length
command: call getRandomPassword()
function-call: getRandomPassword()
expected-pattern: 'missing 1 required positional argument'
```

Expand Down
76 changes: 76 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash

# Accept the repo path as an argument
repo_path=$1

# Run the metacall-deploy command and store the output
output=$(metacall-deploy --inspect OpenAPIv3 --dev --workdir "$repo_path")

# Parse the JSON output using jq to extract the server URL and paths
server_url=$(echo "$output" | jq -r '.[0].servers[0].url')
paths=$(echo "$output" | jq -r '.[0].paths | keys[]')

# Output the server URL and paths
echo "Server URL: $server_url"
echo "Available Paths:"
for path in $paths; do
echo "$server_url$path"
done

# Set the server URL as an environment variable
export SERVER_URL=$server_url


# multi line comment
: ' Exmaple output
[
{
"openapi": "3.0.0",
"info": {
"title": "MetaCall Cloud FaaS deployment 'time-app-web'",
"description": "",
"version": "v1"
},
"servers": [
{
"url": "http://localhost:9000/aa759149a70a/time-app-web/v1",
"description": "MetaCall Cloud FaaS"
}
],
"paths": {
"/call/time": {
"get": {
"summary": "",
"description": "",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/call/index": {
"get": {
"summary": "",
"description": "",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
}
}
]
'
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
PyYAML==6.0.1
config==0.5.1
metacall==0.5.0
PyYAML==6.0.2
19 changes: 0 additions & 19 deletions test-auth-function-mesh.yaml

This file was deleted.

21 changes: 0 additions & 21 deletions test-auth-middleware.yaml

This file was deleted.

8 changes: 4 additions & 4 deletions test-suites/test-string-manipulation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ code-files:
path: examples/string-manipulation/str.rb
test-cases:
- name: Check the longest_repetition function
command: call longest_repetition("aaa")
function-call: longest_repetition("aaa")
expected-pattern: "[ 'a', 3 ]"
- name: Check the longest_repetition function with a different string
command: call longest_repetition("bbbaaabaaaa")
function-call: longest_repetition("bbbaaabaaaa")
expected-pattern: "[ 'a', 4 ]"
- name: Check the longest_repetition function with an empty string
command: call longest_repetition("")
expected-pattern: "NodeJS Loader could not convert the value of type 'Invalid' to N-API | [null, 0]"
function-call: longest_repetition("")
expected-pattern: "NodeJS Loader could not convert the value of type 'Invalid' to N-API || [null, 0]"



Expand Down
10 changes: 5 additions & 5 deletions test-suites/test-time-app-web.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ code-files:
- path: examples/time-app-web/index.py
test-cases:
- name: Check the time is generated in the correct format
command: call time()
function-call: time()
expected-pattern: '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
- name: Check calling the time function with a parameter
command: call time(1)
expected-pattern: 'takes 0 positional arguments but 1 was given'
# - name: Check calling the time function with a parameter
# function-call: time(1)
# expected-pattern: 'takes 0 positional arguments but 1 was given'
- name: Check index.html is fully returned
command: call index()
function-call: index()
expected-pattern: '<html>[\w\W]*</html>'


Expand Down
15 changes: 11 additions & 4 deletions testing.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import argparse

import config

from testing.logger import Logger
from testing.repo_manager import RepoManager
from testing.test_suites_extractor import TestSuitesExtractor
from testing.test_runner import TestRunner
from testing.logger import Logger
from testing.test_suites_extractor import TestSuitesExtractor


def main():
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--file", action="store", help="the test suite file name")
parser.add_argument("-V", "--verbose", action="store_true", help="increase output verbosity")
parser.add_argument("-e", "--environments", nargs="+", default=["cli"], help="the environments to run the tests")
args = parser.parse_args()

logger = Logger.get_instance()
Expand All @@ -20,16 +25,18 @@ def main():
if not test_suite_file_name:
logger.error("Error: test suite file name is required!")


test_suites_extractor = TestSuitesExtractor(test_suite_file_name)
project_name, repo_url, test_suites = test_suites_extractor.extract_test_suites()
config.project_name = project_name

logger.info(f"Project: {project_name}")

repo_manager = RepoManager(repo_url)
repo_manager.clone_repo_if_not_exist()

test_runner = TestRunner(["cli"])
test_runner.run_tests(project_name, test_suites)
test_runner = TestRunner(args.environments)
test_runner.run_tests(test_suites)

if __name__ == "__main__":
main()
5 changes: 3 additions & 2 deletions testing/runner/cli_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ def get_runtime_tag(self, file_name):
else:
raise ValueError("Error: file extension not supported!")

def run_test_command(self, file_path, test_case_command):
def run_test_command(self, file_path, function_call):
file_name = file_path.split('/')[-1]
function_call = 'call ' + function_call
try:
if platform.system() == 'Windows':
process = subprocess.Popen(['metacall.bat'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
process = subprocess.Popen(['metacall'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

commands = ['load ' + ' ' + self.get_runtime_tag(file_name) + ' ' + file_path, test_case_command, 'exit']
commands = ['load ' + ' ' + self.get_runtime_tag(file_name) + ' ' + file_path, function_call, 'exit']
commands = '\n'.join(commands) + '\n' # join the commands with a newline character

process.stdin.write(f"{commands}".encode('utf-8'))
Expand Down
57 changes: 52 additions & 5 deletions testing/runner/faas_interface.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
import json
import subprocess

import os

from testing.runner.runner_interface import RunnerInterface


class FaaSInterface(RunnerInterface):
def __init__(self):
pass
try:
# Get the base URL from the environment variable SERVER_URL
self.base_url = os.environ['SERVER_URL']
except KeyError:
# If the environment variable is not set, return an error
raise KeyError("SERVER_URL environment variable not set, make sure to run 'source ./deploy.sh /path/to/repo' before running the tests")

def get_name(self):
return "faas"

def get_request(self, url):
command = f"curl {url} -X GET"
return command

def post_request(self, url, params):
try:
params = json.loads(params)
except json.JSONDecodeError:
pass

data = json.dumps(params) if isinstance(params, dict) else params
command = f"curl {url} -X POST --data '{data}'"
return command


def parse_function_call(self, function_call):
if '(' in function_call and ')' in function_call:
function_name = function_call.split('(')[0]
params = function_call.split('(')[1].split(')')[0]
params = params if params else None
else:
function_name = function_call
params = None

return function_name, params

def run_test_command(self, file_path, function_call):
function_name, params = self.parse_function_call(function_call)
url = f"{self.base_url}/call/{function_name}"

if params:
command = self.post_request(url, params)
else:
command = self.get_request(url)
print("Command:", command)

result = subprocess.run(command, capture_output=True, text=True, shell=True, check=False)
out_str = result.stdout.strip()

def run_test_command(self, file_path, test_case_command):
# Implement the FaaS call here
# For now, return a placeholder string
return "FaaS output placeholder"
return out_str
2 changes: 1 addition & 1 deletion testing/runner/runner_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

class RunnerInterface(ABC):
@abstractmethod
def run_test_command(self, file_path, test_case_command):
def run_test_command(self, file_path, function_call):
pass
10 changes: 5 additions & 5 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

class TestCaseGenerator:
@staticmethod
def create_test_method(interface, test_case_name, test_case_command, test_case_expected_stdout):
def create_test_method(interface, test_case_name, function_call, test_case_expected_stdout):
def test_method(self):
out_str = interface.run_test_command(self.file_path, test_case_command)
out_str = interface.run_test_command(self.file_path, function_call)
passed = self.check_match(out_str, test_case_expected_stdout)
self.assertTrue(passed, f"{interface.get_name()}_{test_case_name} - Expected: {test_case_expected_stdout}, Actual: {out_str}")
return test_method
Expand Down Expand Up @@ -37,9 +37,9 @@ def check_match(actual, expected_pattern):
return TestCaseGenerator.check_match(actual, expected_pattern)

for test_case in test_cases:
test_case_name, test_case_command, test_case_expected_stdout = test_case
test_case_name, function_call, test_case_expected_stdout = test_case
for interface in self.interfaces:
test_method = TestCaseGenerator.create_test_method(interface, test_case_name, test_case_command, test_case_expected_stdout)
test_method = TestCaseGenerator.create_test_method(interface, test_case_name, function_call, test_case_expected_stdout)
test_method_name = f'testCase_{interface.get_name()}_{test_case_name}'
setattr(DynamicTestSuite, test_method_name, test_method)

Expand All @@ -61,7 +61,7 @@ def create_project_test_suites(self, test_suites):
master_suite.addTests(test_loader.loadTestsFromTestCase(test_suite))
return master_suite

def run_tests(self, project_name, test_suites):
def run_tests(self, test_suites):
master_suite = self.create_project_test_suites(test_suites)
runner = unittest.TextTestRunner(verbosity=self.test_verbosity)
result = runner.run(master_suite)
Expand Down
2 changes: 1 addition & 1 deletion testing/test_suites_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def extract_test_suites(self):
code_files = data['code-files']
test_suites = []
for code_file in code_files:
test_cases = [(test_case['name'], test_case['command'], test_case['expected-pattern']) for test_case in code_file['test-cases']]
test_cases = [(test_case['name'], test_case['function-call'], test_case['expected-pattern']) for test_case in code_file['test-cases']]
test_suites.append((code_file['path'], test_cases))
except KeyError as e:
self.logger.error(f"Error: parsing yaml file, missing key:{e}")
Expand Down