mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00

Running multiple ngrok concurrently is only allowed from pro and up (OOTB and without shenanigans) is only allowed from Pro and up. However multiple tunnels through a single ngrok is allowed -> when tunneling through ngrok, start the process without any tunnel, use the API to create then remove the local tunnel, and shut down ngrok IIF there's no tunnel left. There's plenty of race conditions but given how slow tests are when they involve github that's probably not an issue.
143 lines
5.0 KiB
Python
143 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
import configparser
|
|
import re
|
|
import subprocess
|
|
import time
|
|
|
|
import psutil
|
|
import pytest
|
|
import requests
|
|
|
|
NGROK_CLI = [
|
|
'ngrok', 'start', '--none', '--region', 'eu',
|
|
]
|
|
|
|
def pytest_addoption(parser):
|
|
parser.addoption(
|
|
'--tunnel', action="store", type="choice", choices=['ngrok', 'localtunnel'], default='ngrok',
|
|
help="Which tunneling method to use to expose the local Odoo server "
|
|
"to hook up github's webhook. ngrok is more reliable, but "
|
|
"creating a free account is necessary to avoid rate-limiting "
|
|
"issues (anonymous limiting is rate-limited at 20 incoming "
|
|
"queries per minute, free is 40, multi-repo batching tests will "
|
|
"blow through the former); localtunnel has no rate-limiting but "
|
|
"the servers are way less reliable")
|
|
|
|
@pytest.fixture(scope="session")
|
|
def config(pytestconfig):
|
|
""" Flat version of the pytest config file (pytest.ini), parses to a
|
|
simple dict of {section: {key: value}}
|
|
|
|
"""
|
|
conf = configparser.ConfigParser(interpolation=None)
|
|
conf.read([pytestconfig.inifile])
|
|
return {
|
|
name: dict(s.items())
|
|
for name, s in conf.items()
|
|
}
|
|
|
|
@pytest.fixture(scope='session')
|
|
def rolemap(config):
|
|
# only fetch github logins once per session
|
|
rolemap = {}
|
|
for k, data in config.items():
|
|
if k.startswith('role_'):
|
|
role = k[5:]
|
|
elif k == 'github':
|
|
role = 'user'
|
|
else:
|
|
continue
|
|
|
|
r = requests.get('https://api.github.com/user', headers={'Authorization': 'token %s' % data['token']})
|
|
r.raise_for_status()
|
|
|
|
rolemap[role] = data['user'] = r.json()['login']
|
|
return rolemap
|
|
|
|
# apparently conftests can override one another's fixtures but plugins can't
|
|
# override conftest fixtures (?) so if this is defined as "users" it replaces
|
|
# the one from runbot_merge/tests/local and everything breaks.
|
|
#
|
|
# Alternatively this could be special-cased using remote_p or something but
|
|
# that's even more gross. It might be possible to handle that via pytest's
|
|
# hooks as well but I didn't check
|
|
@pytest.fixture
|
|
def users_(env, config, rolemap):
|
|
for role, login in rolemap.items():
|
|
if role in ('user', 'other'):
|
|
continue
|
|
|
|
env['res.partner'].create({
|
|
'name': config['role_' + role].get('name', login),
|
|
'github_login': login,
|
|
'reviewer': role == 'reviewer',
|
|
'self_reviewer': role == 'self_reviewer',
|
|
})
|
|
|
|
return rolemap
|
|
|
|
@pytest.fixture(scope='session')
|
|
def tunnel(pytestconfig, port):
|
|
""" Creates a tunnel to localhost:<port> using ngrok or localtunnel, should yield the
|
|
publicly routable address & terminate the process at the end of the session
|
|
"""
|
|
|
|
tunnel = pytestconfig.getoption('--tunnel')
|
|
if tunnel == 'ngrok':
|
|
addr = 'localhost:%d' % port
|
|
# if ngrok is not running, start it
|
|
try:
|
|
# FIXME: use config file so we can set web_addr to something else
|
|
# than localhost:4040 (otherwise we can't disambiguate
|
|
# between the ngrok we started and an ngrok started by
|
|
# some other user)
|
|
requests.get('http://localhost:4040/api')
|
|
except requests.exceptions.ConnectionError:
|
|
subprocess.Popen(NGROK_CLI, stdout=subprocess.DEVNULL)
|
|
time.sleep(1)
|
|
|
|
requests.post('http://localhost:4040/api/tunnels', json={
|
|
'name': str(port),
|
|
'proto': 'http',
|
|
'bind_tls': True,
|
|
'addr': addr,
|
|
'inspect': False,
|
|
})
|
|
time.sleep(5)
|
|
|
|
try:
|
|
r = requests.get('http://localhost:4040/api/tunnels')
|
|
r.raise_for_status()
|
|
yield next(
|
|
t['public_url']
|
|
for t in r.json()['tunnels']
|
|
if t['proto'] == 'https'
|
|
if t['config']['addr'].endswith(addr)
|
|
)
|
|
finally:
|
|
requests.delete('http://localhost:4040/api/tunnels/%s' % port)
|
|
time.sleep(5) # apparently tearing down the tunnel can take some time
|
|
r = requests.get('http://localhost:4040/api/tunnels')
|
|
if r.ok and r.json()['tunnels']:
|
|
return
|
|
|
|
# ngrok is broken or all tunnels have been shut down -> try to
|
|
# find and kill it (but only if it looks a lot like we started it)
|
|
for p in psutil.process_iter():
|
|
if p.name() == 'ngrok' and p.cmdline() == NGROK_CLI:
|
|
p.terminate()
|
|
break
|
|
|
|
elif tunnel == 'localtunnel':
|
|
p = subprocess.Popen(['lt', '-p', str(port)], stdout=subprocess.PIPE)
|
|
try:
|
|
r = p.stdout.readline()
|
|
m = re.match(br'your url is: (https://.*\.localtunnel\.me)', r)
|
|
assert m, "could not get the localtunnel URL"
|
|
yield m.group(1).decode('ascii')
|
|
finally:
|
|
p.terminate()
|
|
p.wait(30)
|
|
else:
|
|
raise ValueError("Unsupported %s tunnel method" % tunnel)
|