Skip to content

Commit 1ccb803

Browse files
committed
ci: Run tests on MySQL and PostgreSQL as well
1 parent 4e222be commit 1ccb803

File tree

7 files changed

+223
-61
lines changed

7 files changed

+223
-61
lines changed

.github/workflows/main.yaml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
runs-on: ubuntu-20.04
1818
strategy:
1919
matrix:
20+
# Other backends run only for single PHP version to reduce the CI costs.
21+
storage:
22+
- 'sqlite'
2023
php:
2124
- '8.0'
2225
- '7.4'
@@ -29,7 +32,11 @@ jobs:
2932
- php: '7.0'
3033
cs_fixer: true
3134
lint_js: true
32-
name: 'Check with PHP ${{ matrix.php }}'
35+
- php: '8.0'
36+
storage: mysql
37+
- php: '8.0'
38+
storage: postgresql
39+
name: 'Check with PHP ${{ matrix.php }} and ${{ matrix.storage }} storage backend'
3340
steps:
3441
- uses: actions/checkout@v2
3542

@@ -43,7 +50,9 @@ jobs:
4350
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
4451

4552
- name: Update flake.nix to match the current CI job from matrix
46-
run: sed -i 's/matrix.phpPackage = "php";/matrix.phpPackage = builtins.replaceStrings ["."] [""] "php${{ matrix.php }}";/' flake.nix
53+
run: |
54+
sed -i 's/matrix.phpPackage = "php";/matrix.phpPackage = builtins.replaceStrings ["."] [""] "php${{ matrix.php }}";/' flake.nix
55+
sed -i 's/matrix.storage = "all";/matrix.storage = "${{ matrix.storage }}";/' flake.nix
4756
4857
- name: Cache Node modules
4958
uses: actions/cache@v2
@@ -87,7 +96,7 @@ jobs:
8796
run: nix-shell --run 'npm run test:server'
8897

8998
- name: Run integration tests
90-
run: nix-shell --run 'npm run test:integration'
99+
run: SELFOSS_TEST_STORAGE_BACKEND=${{ matrix.storage }} nix-shell --run 'npm run test:integration'
91100

92101
deploy:
93102
name: 'Upload artefacts'

flake.nix

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
# By default, we use the default PHP version from Nixpkgs.
2525
matrix.phpPackage = "php";
26+
27+
# We install all storage backends by default.
28+
matrix.storage = "all";
2629
in
2730
# For each supported platform,
2831
utils.lib.eachDefaultSystem (system:
@@ -41,6 +44,14 @@
4144
bcrypt
4245
requests
4346
]);
47+
48+
# Database servers for testing.
49+
dbServers = {
50+
mysql = [ pkgs.mariadb ];
51+
postgresql = [ pkgs.postgresql ];
52+
sqlite = [ ];
53+
all = builtins.concatLists (builtins.attrValues (builtins.removeAttrs dbServers [ "all" ]));
54+
};
4455
in
4556
{
4657
# Expose shell environment for development.
@@ -65,11 +76,7 @@
6576

6677
# Website generator.
6778
pkgs.zola
68-
69-
# Database servers for testing.
70-
pkgs.mariadb
71-
pkgs.postgresql
72-
];
79+
] ++ dbServers.${matrix.storage};
7380

7481
# node-gyp wants some locales, let’s make them available through an environment variable.
7582
LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive";

tests/integration/helpers/integration.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import os
12
import time
23
import unittest
34
from pathlib import Path
45
from .data_server import DataServerThread
6+
from .storage_servers import MySQL, PostgreSQL, SQLite
57
from .selfoss_server import SelfossServerThread
68

79

@@ -20,6 +22,18 @@ def setUp(self):
2022
self.selfoss_username = 'admin'
2123
self.selfoss_password = 'hunter2'
2224

25+
storage_backend = os.environ.get('SELFOSS_TEST_STORAGE_BACKEND', 'sqlite')
26+
if storage_backend == 'mysql':
27+
self.storage_server = MySQL()
28+
elif storage_backend == 'postgresql':
29+
self.storage_server = PostgreSQL()
30+
elif storage_backend == 'sqlite':
31+
self.storage_server = SQLite()
32+
else:
33+
raise Exception(f'Unknown storage backend type: {storage_backend}')
34+
35+
self.storage_server.start()
36+
2337
self.selfoss_root = current_dir.parent.parent.parent
2438

2539
self.selfoss_thread = SelfossServerThread(
@@ -28,6 +42,7 @@ def setUp(self):
2842
username=self.selfoss_username,
2943
host_name=self.selfoss_host_name,
3044
port=self.selfoss_port,
45+
storage_config=self.storage_server.get_config(),
3146
)
3247
self.selfoss_thread.start()
3348

@@ -42,5 +57,6 @@ def setUp(self):
4257

4358
def tearDown(self):
4459
self.selfoss_thread.stop()
60+
self.storage_server.stop()
4561
self.data_server_thread.stop()
4662

tests/integration/helpers/selfoss_server.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,35 @@
44
import tempfile
55
import threading
66
from pathlib import Path
7+
from typing import Dict
78

89

910
class SelfossServerThread(threading.Thread):
1011
'''
1112
A thread that starts and stops PHP’s built-in web server running selfoss.
1213
'''
13-
def __init__(self, selfoss_root: Path, username: str, password: str, host_name: str, port: int):
14+
def __init__(
15+
self,
16+
selfoss_root: Path,
17+
username: str,
18+
password: str,
19+
host_name: str,
20+
port: int,
21+
storage_config: Dict[str, str],
22+
):
1423
super().__init__()
1524
self.selfoss_root = selfoss_root
1625
self.username = username
1726
self.password = password
1827
self.host_name = host_name
1928
self.port = port
29+
self.storage_config = storage_config
2030

2131
def run(self):
2232
with tempfile.TemporaryDirectory() as temp_dir:
2333
# Set up data directories.
2434
temp_dir = Path(temp_dir)
2535
data_dir = temp_dir / 'data'
26-
(data_dir / 'sqlite').mkdir(parents=True)
2736
(data_dir / 'thumbnails').mkdir(parents=True)
2837
(data_dir / 'favicons').mkdir(parents=True)
2938

@@ -39,6 +48,9 @@ def run(self):
3948
'SELFOSS_LOGGER_LEVEL': 'DEBUG',
4049
}
4150

51+
for key, value in self.storage_config.items():
52+
test_env[f'SELFOSS_{key.upper()}'] = value
53+
4254
current_dir = Path(__file__).parent.absolute()
4355

4456
php_command = [
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import argparse
2+
import abc
3+
import signal
4+
import subprocess
5+
import sys
6+
import tempfile
7+
import time
8+
from abc import ABC
9+
from pathlib import Path
10+
from threading import Event
11+
12+
13+
class Storage(ABC):
14+
pass
15+
16+
17+
class MySQL(Storage):
18+
def __init__(self):
19+
# Set up data directories.
20+
self.temp_dir = tempfile.TemporaryDirectory()
21+
temp_dir = Path(self.temp_dir.name)
22+
self.db_dir = temp_dir / 'data'
23+
self.db_dir.mkdir()
24+
self.db_dir.chmod(0o750)
25+
26+
self.socket_path = temp_dir / 'mysqld.sock'
27+
self.user = 'selfoss'
28+
self.password = 'password'
29+
self.database = 'selfoss'
30+
31+
def start(self):
32+
subprocess.check_call([
33+
'mysql_install_db',
34+
# Prevent defaulting to --user=mysql.
35+
'--no-defaults',
36+
f'--datadir={self.db_dir}',
37+
])
38+
39+
# Start the server
40+
subprocess.check_call([
41+
'mysqld_safe',
42+
# Prevent trying to use /var/log for logs.
43+
'--no-defaults',
44+
f'--datadir={self.db_dir}',
45+
f'--socket={self.socket_path}',
46+
'--skip-networking',
47+
'--no-auto-restart',
48+
])
49+
50+
# Create user and database.
51+
# Waiting does not seem to work.
52+
time.sleep(2)
53+
subprocess.check_call(['mysql', '--wait', f'--socket={self.socket_path}', f"--execute=CREATE USER '{self.user}'@'localhost' IDENTIFIED BY '{self.password}';"])
54+
subprocess.check_call(['mysql', '--wait', f'--socket={self.socket_path}', f'--execute=CREATE DATABASE {self.database};'])
55+
subprocess.check_call(['mysql', '--wait', f'--socket={self.socket_path}', f"--execute=GRANT ALL PRIVILEGES ON *.* TO '{self.user}'@'localhost';"])
56+
57+
def stop(self):
58+
subprocess.check_call(['mysqladmin', f'--socket={self.socket_path}', 'shutdown'])
59+
self.temp_dir.cleanup()
60+
61+
def get_config(self):
62+
return {
63+
'db_type': 'mysql',
64+
'db_socket': self.socket_path,
65+
'db_username': self.user,
66+
'db_password': self.password,
67+
'db_database': self.database,
68+
}
69+
70+
71+
class PostgreSQL(Storage):
72+
def __init__(self):
73+
# Set up data directories.
74+
self.temp_dir = tempfile.TemporaryDirectory()
75+
temp_dir = Path(self.temp_dir.name)
76+
self.db_dir = temp_dir / 'data'
77+
self.db_dir.mkdir()
78+
self.db_dir.chmod(0o750)
79+
80+
self.socket_dir_path = temp_dir
81+
self.user = 'selfoss'
82+
self.database = 'selfoss'
83+
84+
def start(self):
85+
subprocess.check_call(['initdb', self.db_dir])
86+
87+
# Start the server
88+
subprocess.check_call([
89+
'pg_ctl',
90+
'start',
91+
f'--pgdata={self.db_dir}',
92+
# Intentionally passing options as a string
93+
f'--options=-k {self.socket_dir_path} -c listen_addresses=',
94+
])
95+
96+
# Create users
97+
# Using a “template1” database since it is guaranteed to be present.
98+
subprocess.check_call(['psql', f'--host={self.socket_dir_path}', '--dbname=template1', '--tuples-only', '--no-align', f'--command=CREATE USER "{self.user}"'])
99+
subprocess.check_call(['psql', f'--host={self.socket_dir_path}', '--dbname=template1', '--tuples-only', '--no-align', f'--command=CREATE DATABASE "{self.database}" WITH OWNER = "{self.user}"'])
100+
101+
def stop(self):
102+
subprocess.check_call(['pg_ctl', 'stop', f'--pgdata={self.db_dir}'])
103+
self.temp_dir.cleanup()
104+
105+
def get_config(self):
106+
return {
107+
'db_type': 'pgsql',
108+
'db_host': self.socket_dir_path,
109+
'db_username': self.user,
110+
'db_database': self.database,
111+
}
112+
113+
114+
class SQLite(Storage):
115+
def __init__(self):
116+
# Set up data directory.
117+
self.temp_dir = tempfile.TemporaryDirectory()
118+
temp_dir = Path(self.temp_dir.name)
119+
self.file = temp_dir / 'selfoss.db'
120+
121+
def start(self):
122+
pass
123+
124+
def stop(self):
125+
self.temp_dir.cleanup()
126+
127+
def get_config(self):
128+
return {
129+
'db_type': 'sqlite',
130+
'db_file': self.file,
131+
}
132+
133+
134+
if __name__ == '__main__':
135+
parser = argparse.ArgumentParser(
136+
description='Runs a storage server for purposes of testing',
137+
)
138+
parser.add_argument(
139+
'backend',
140+
nargs='?',
141+
default='sqlite',
142+
help='Database backend to start'
143+
)
144+
args = parser.parse_args()
145+
storage_backend = args.backend
146+
147+
if storage_backend == 'mysql':
148+
storage_server = MySQL()
149+
elif storage_backend == 'postgresql':
150+
storage_server = PostgreSQL()
151+
elif storage_backend == 'sqlite':
152+
storage_server = SQLite()
153+
else:
154+
raise Exception(f'Unknown storage backend type: {storage_backend}')
155+
156+
termination_event = Event()
157+
158+
def exit_gracefully(*args):
159+
storage_server.stop()
160+
termination_event.set()
161+
162+
signal.signal(signal.SIGINT, exit_gracefully)
163+
signal.signal(signal.SIGTERM, exit_gracefully)
164+
165+
storage_server.start()
166+
167+
print(storage_server.get_config())
168+
169+
termination_event.wait()

tests/utils/mysql.sh

Lines changed: 0 additions & 24 deletions
This file was deleted.

tests/utils/postgresql.sh

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)