chore: sync все локальные изменения (auto, 2026-05-04)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# План тестирования: загрузка функций из архива (Fission Console)
|
||||
|
||||
Дата: 2026-05-04
|
||||
|
||||
## Цели
|
||||
- Проверить корректность загрузки функций из архива для всех поддерживаемых языков (Python, Node.js, Ruby, PHP)
|
||||
- Проверить работу линтера, валидацию entrypoint, соответствие языка, структуру архива
|
||||
- Проверить обработку ошибок, edge-cases, граничные размеры
|
||||
|
||||
## Категории тестов
|
||||
|
||||
### 1. Валидные случаи
|
||||
- [ ] Python: demo.py с def main(), entrypoint demo.main
|
||||
- [ ] Node.js: demo.js с module.exports = main, entrypoint demo.main
|
||||
- [ ] Ruby: demo.rb с def main, entrypoint demo.main
|
||||
- [ ] PHP: demo.php с function main, entrypoint demo.main
|
||||
- [ ] Архив с несколькими файлами (несколько языков, только один выбран)
|
||||
- [ ] Архив с подпапками (файл demo.py внутри src/)
|
||||
- [ ] Entrypoint с нестандартным именем (myfunc.py, entrypoint myfunc.main)
|
||||
|
||||
### 2. Ошибочные случаи
|
||||
- [ ] Нет файла с нужным именем (entrypoint demo.main, а demo.py отсутствует)
|
||||
- [ ] Нет функции с нужным именем (demo.py есть, но нет def main)
|
||||
- [ ] Язык выбран Python, а в архиве только .js
|
||||
- [ ] Язык выбран Node.js, а в архиве только .py
|
||||
- [ ] Entrypoint пустой
|
||||
- [ ] Entrypoint без точки ("main")
|
||||
- [ ] Архив пустой
|
||||
- [ ] Архив > 100 KB
|
||||
- [ ] Суммарный размер файлов > 100 KB (zip bomb)
|
||||
- [ ] Файл с синтаксической ошибкой (Python, JS, Ruby, PHP)
|
||||
|
||||
### 3. Пограничные случаи
|
||||
- [ ] demo.py ровно 100 KB
|
||||
- [ ] Архив с demo.py и demo.js, язык Python, entrypoint demo.main
|
||||
- [ ] Архив с demo.py и demo.js, язык Node.js, entrypoint demo.main
|
||||
- [ ] demo.py с def main(event, context)
|
||||
- [ ] demo.py с def main() и def main2()
|
||||
- [ ] demo.py с def main : (без скобок)
|
||||
- [ ] demo.py с декоратором @fission_entry
|
||||
- [ ] demo.py с не-ASCII именем функции
|
||||
|
||||
### 4. UI/UX
|
||||
- [ ] Проверка сообщений линтера (ошибки, предупреждения, OK)
|
||||
- [ ] Проверка отображения имени архива, entrypoint, языка
|
||||
- [ ] Проверка что после ошибки можно загрузить исправленный архив
|
||||
|
||||
## Автоматизация
|
||||
- Тесты реализовать на Go (backend), Python (pytest), JS (playwright) — по необходимости
|
||||
- Для каждого теста: архив, ожидаемый результат, шаги UI/REST, ожидаемое сообщение
|
||||
|
||||
---
|
||||
|
||||
## Ход выполнения
|
||||
- [ ] Подготовить тестовые архивы
|
||||
- [ ] Реализовать автотесты (минимум: backend REST, опционально UI)
|
||||
- [ ] Зафиксировать результаты в doc/reports/
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM ghcr.io/fission/python-env:latest
|
||||
COPY server.py /app/server.py
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import json
|
||||
|
||||
from flask import Flask, request, abort
|
||||
from gevent.pywsgi import WSGIServer
|
||||
import bjoern
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||
|
||||
from flask_sockets import Sockets
|
||||
|
||||
IS_PY2 = (sys.version_info.major == 2)
|
||||
SENTRY_DSN = os.environ.get('SENTRY_DSN', None)
|
||||
SENTRY_RELEASE = os.environ.get('SENTRY_RELEASE', None)
|
||||
USERFUNCVOL = os.environ.get("USERFUNCVOL", "/userfunc")
|
||||
RUNTIME_PORT = int(os.environ.get("RUNTIME_PORT", "8888"))
|
||||
|
||||
if SENTRY_DSN:
|
||||
params = {'dsn': SENTRY_DSN, 'integrations': [FlaskIntegration()]}
|
||||
if SENTRY_RELEASE:
|
||||
params['release'] = SENTRY_RELEASE
|
||||
sentry_sdk.init(**params)
|
||||
|
||||
|
||||
def import_src(path):
|
||||
if IS_PY2:
|
||||
import imp
|
||||
return imp.load_source('mod', path)
|
||||
else:
|
||||
return importlib.machinery.SourceFileLoader('mod', path).load_module()
|
||||
|
||||
|
||||
def store_specialize_info(state):
|
||||
json.dump(state, open(os.path.join(USERFUNCVOL, "state.json"), "w"))
|
||||
|
||||
|
||||
def check_specialize_info_exists():
|
||||
return os.path.exists(os.path.join(USERFUNCVOL, "state.json"))
|
||||
|
||||
|
||||
def read_specialize_info():
|
||||
return json.load(open(os.path.join(USERFUNCVOL, "state.json")))
|
||||
|
||||
|
||||
def remove_specialize_info():
|
||||
os.remove(os.path.join(USERFUNCVOL, "state.json"))
|
||||
|
||||
|
||||
class SignalExit(SystemExit):
|
||||
|
||||
def __init__(self, signo, exccode=1):
|
||||
super(SignalExit, self).__init__(exccode)
|
||||
self.signo = signo
|
||||
|
||||
|
||||
def register_signal_handlers(signal_handler=signal.SIG_DFL):
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
|
||||
def _count_required_params(func):
|
||||
"""Return number of required (non-default) positional parameters."""
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
count = 0
|
||||
for p in sig.parameters.values():
|
||||
if p.kind in (inspect.Parameter.VAR_POSITIONAL,
|
||||
inspect.Parameter.VAR_KEYWORD):
|
||||
continue
|
||||
if p.default is inspect.Parameter.empty:
|
||||
count += 1
|
||||
return count
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
class FuncApp(Flask):
|
||||
|
||||
def __init__(self, name, loglevel=logging.DEBUG):
|
||||
super(FuncApp, self).__init__(name)
|
||||
|
||||
self.userfunc = None
|
||||
self.root = logging.getLogger()
|
||||
self.ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
self.root.setLevel(loglevel)
|
||||
self.ch.setLevel(loglevel)
|
||||
self.ch.setFormatter(
|
||||
logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
||||
self.logger.addHandler(self.ch)
|
||||
|
||||
if check_specialize_info_exists():
|
||||
self.logger.info('Found state.json')
|
||||
specialize_info = read_specialize_info()
|
||||
self.userfunc = self._load_v2(specialize_info)
|
||||
self.logger.info('Loaded user function {}'.format(specialize_info))
|
||||
|
||||
def load(self):
|
||||
self.logger.info('/specialize called')
|
||||
self.userfunc = import_src('/userfunc/user').main
|
||||
return ""
|
||||
|
||||
def loadv2(self):
|
||||
specialize_info = request.get_json()
|
||||
if check_specialize_info_exists():
|
||||
self.logger.warning("Found state.json, overwriting")
|
||||
self.userfunc = self._load_v2(specialize_info)
|
||||
store_specialize_info(specialize_info)
|
||||
return ""
|
||||
|
||||
def healthz(self):
|
||||
return "", 200
|
||||
|
||||
def userfunc_call(self, *args, **kwargs):
|
||||
"""
|
||||
Call user function with compatible signature detection:
|
||||
def main() -> main()
|
||||
def main(event, context) -> main(request, {})
|
||||
def main(request) -> main(request)
|
||||
def main(event=None, ...) -> main() (all defaults)
|
||||
"""
|
||||
if self.userfunc is None:
|
||||
self.logger.error('userfunc is None')
|
||||
return abort(500)
|
||||
|
||||
required = _count_required_params(self.userfunc)
|
||||
|
||||
if required == 0:
|
||||
# def main() or def main(event=None, context=None) — call with no args
|
||||
return self.userfunc()
|
||||
elif required == 1:
|
||||
# def main(request) style
|
||||
return self.userfunc(request)
|
||||
elif required >= 2:
|
||||
# def main(event, context) — AWS Lambda / Kubeless style
|
||||
# pass flask request as event, empty dict as context
|
||||
return self.userfunc(request, {})
|
||||
else:
|
||||
return self.userfunc(*args)
|
||||
|
||||
def _load_v2(self, specialize_info):
|
||||
filepath = specialize_info['filepath']
|
||||
handler = specialize_info['functionName']
|
||||
self.logger.info(
|
||||
'specialize called with filepath = "{}" handler = "{}"'.format(
|
||||
filepath, handler))
|
||||
parts = handler.rsplit(".", 1)
|
||||
if len(handler) == 0:
|
||||
moduleName = 'main'
|
||||
funcName = 'main'
|
||||
elif len(parts) == 1:
|
||||
moduleName = 'main'
|
||||
funcName = parts[0]
|
||||
else:
|
||||
moduleName = parts[0]
|
||||
funcName = parts[1]
|
||||
self.logger.debug('moduleName = "{}" funcName = "{}"'.format(
|
||||
moduleName, funcName))
|
||||
|
||||
if os.path.isdir(filepath):
|
||||
sys.path.append(filepath)
|
||||
self.logger.debug('__package__ = "{}"'.format(__package__))
|
||||
if __package__:
|
||||
mod = importlib.import_module(moduleName, __package__)
|
||||
else:
|
||||
mod = importlib.import_module(moduleName)
|
||||
else:
|
||||
mod = import_src(filepath)
|
||||
|
||||
return getattr(mod, funcName)
|
||||
|
||||
def signal_handler(self, signalnum, frame):
|
||||
self.logger.info('Received signal {}'.format(
|
||||
signal.strsignal(signalnum)))
|
||||
if check_specialize_info_exists():
|
||||
self.logger.info('Found state.json, removing')
|
||||
remove_specialize_info()
|
||||
signal.signal(signalnum, signal.SIG_DFL)
|
||||
raise SignalExit(signalnum)
|
||||
|
||||
|
||||
def main():
|
||||
app = FuncApp(__name__, logging.DEBUG)
|
||||
sockets = Sockets(app)
|
||||
register_signal_handlers(app.signal_handler)
|
||||
|
||||
app.add_url_rule('/specialize', 'load', app.load, methods=['POST'])
|
||||
app.add_url_rule('/v2/specialize', 'loadv2', app.loadv2, methods=['POST'])
|
||||
app.add_url_rule('/healthz', 'healthz', app.healthz, methods=['GET'])
|
||||
app.add_url_rule(
|
||||
'/',
|
||||
'userfunc_call',
|
||||
app.userfunc_call,
|
||||
methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE'])
|
||||
app.add_url_rule(
|
||||
'/<path:path>',
|
||||
'userfunc_call',
|
||||
app.userfunc_call,
|
||||
methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE'])
|
||||
sockets.add_url_rule(
|
||||
'/',
|
||||
'userfunc_call',
|
||||
app.userfunc_call,
|
||||
methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE'])
|
||||
|
||||
if os.environ.get("WSGI_FRAMEWORK") == "GEVENT":
|
||||
app.logger.info("Starting gevent based server")
|
||||
from gevent_ws import WebSocketHandler
|
||||
svc = WSGIServer(('0.0.0.0', RUNTIME_PORT),
|
||||
app,
|
||||
handler_class=WebSocketHandler)
|
||||
svc.serve_forever()
|
||||
else:
|
||||
app.logger.info("Starting bjoern based server")
|
||||
bjoern.run(app, '0.0.0.0', RUNTIME_PORT, reuse_port=True)
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5qc1VUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBub3RNYWluKCkgeyByZXR1cm4gIk5vdCBNYWluIjt9ClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAAAAAAAAAAACkgQAAAABkZW1vLmpzVVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAUQAAAAAA
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5qc1VUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBtYWluKCkgeyByZXR1cm4gIkphdmFTY3JpcHQgT0siOyB9CmZ1bmN0aW9uIGJhZCgpIHJldHVybiAiZXJyb3IiOwpQSwECFAAUAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5qc1VUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5qc1VUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBtYWluKCkgeyByZXR1cm4gIkphdmFTY3JpcHQgT0siOyB9ClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAAAAAAAAAAACkgQAAAABkZW1vLmpzVVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAUQAAAAAA
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAKAAQAZGVtby5waHBVVAkAA0MEG2dDBRtnYXgLAAEE9QEAAAQcADw/cGhwCmlmKCFmdW5jdGlvbl9leGlzdHMoIm1haW4iKSkgewogIGZ1bmN0aW9uIG1haW4oKSB7IHJldHVybiAiUEhQIG9rIjsgfQp9ClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAKAAQAAAAAAAAAAACkgQAAAABkZW1vLnBocFVUBQADQgQbd2F4CwABBPUBAAAEHAAAAFBLAwQUAAAACACtW+pUAAAAAAAAAAAAAAAAAAwAAAHB5LWhlbGxvLnB5VVQJAANCBBtnQwUbZ3V4CwABBPUBAAAEHABkZWYgbWFpbigpOgogICAgcmV0dXJuICJIZWxsbyBvayIKClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAACkgQAAAHB5LWhlbGxvLnB5VVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAWgAAAAAA
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwA
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW4yKCk6CiAgICByZXR1cm4gIk5vdCBNYWluIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5weVVUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW4oKToKICAgIHJldHVybiAiS29ycmVrdCIKZGVmIGJhZCgpCiAgICByZXR1cm4gIkVycm9yIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5weVVUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW4oKToKICAgIHJldHVybiAiS29ycmVrdCIKZGVmIGJhZCgpCiAgICByZXR1cm4gIkVycm9yIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5weVVUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAY29kZSA9ICJkZWNvcmF0b3IiCkBkZWNvcmF0b3IKZGVmIG1haW4oKToKICAgIHJldHVybiAiZGVjb3JhdG9yIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5weVVUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW46CiAgICByZXR1cm4gIkNvbG9uIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5weVVUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW4oKToKICAgIHJldHVybiAiS29ycmVrdCIKZGVmIG1haW4yKCk6CiAgICByZXR1cm4gIk1haW4yIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAkABAAAAAAAAAAAApIEAAAAAZGVtby5weVVUBQADQgQbdXgLAAEE9QEAAAQcAAAAUEsFBgAAAAABAAEATgAAAFEAAAAAAA==
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAY29kZSA9ICLQnNCw0YDQtdC00L7QvCIKZGVmIM6x0L7RgNC+0LkgKCk6CiAgICByZXR1cm4gIm9rIgogClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAAAAAAAAAAACkgQAAAABkZW1vLnB5VVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAUQAAAAAA
|
||||
@@ -0,0 +1,2 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwA
|
||||
ZGVmIG1haW4oKToKICAgIHJldHVybiAiS29ycmVrdCIKClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAAAAAAAAAAACkgQAAAABkZW1vLnB5VVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAUQAAAAAA
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAwAAABzcmMvZGVtby5weVVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW4oKToKICAgIHJldHVybiAiU3ViZGlyIgoKUEsBAhQAFAAAAAgArVvqVAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAACkgQAAAHNyYy9kZW1vLnB5VVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAUQAAAAAA
|
||||
@@ -0,0 +1 @@
|
||||
UEsDBBQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAZGVtby5yYlVUCQADQwQbZ0MFG2d1eAsAAQT1AQAABBwAZGVmIG1haW4KICAgICJydWJ5IG9rIgogClBLAQIUABQAAAAIAK1b6lQAAAAAAAAAAAAAAAAJAAQAAAAAAAAAAACkgQAAAABkZW1vLnJiVVQFAANCBBt1eAsAAQT1AQAABBwAAABQSwUGAAAAAAEAAQBOAAAAUQAAAAAA
|
||||
Reference in New Issue
Block a user