fix(database): qualify RD21 saves against pinned MariaDB 12.2.2

This commit is contained in:
cesnimda
2026-10-24 01:47:50 +02:00
parent 0d16a691e0
commit a2c9bda0d1
30 changed files with 2226 additions and 63 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Run the explicit MariaDB persistence lane, never a SQLite fallback."""
import argparse
import datetime
import json
import os
from pathlib import Path
import re
import shutil
import socket
import subprocess
import uuid
ROOT = Path(__file__).resolve().parent.parent
def compose_command(project):
if not re.fullmatch(r'jobtracker-it-[a-z0-9]+', project):
raise ValueError('Only generated integration projects may be managed.')
return ['docker', 'compose', '--project-name', project, '--project-directory', str(ROOT),
'-f', str(ROOT / 'docker-compose.database.yml'), '--profile', 'integration']
def test_connection(port):
if not 0 < port < 65536: raise ValueError('Invalid integration port.')
return f'Server=127.0.0.1;Port={port};User ID=root;Password=jt-integration-only;SslMode=None'
def existing_connection(environment):
value = environment.get('JOBTRACKER_MARIADB_TEST_ADMIN', '')
if not value.strip(): raise ValueError('--existing-server requires explicit JOBTRACKER_MARIADB_TEST_ADMIN.')
# The fixture independently enforces loopback TCP, exact server version and generated schemas.
return value
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--existing-server', action='store_true', help='Use an explicitly configured isolated loopback server; fixture guards still apply.')
parser.add_argument('--output', default=str(ROOT / '.artifacts/mariadb/integration'))
args = parser.parse_args()
environment = os.environ.copy()
output = Path(args.output).resolve()
output.mkdir(parents=True, exist_ok=True)
project = 'jobtracker-it-' + uuid.uuid4().hex
compose = compose_command(project)
managed = not args.existing_server
started = datetime.datetime.now(datetime.timezone.utc).isoformat()
commands = []
code = 1
def run(command, name):
commands.append({'command': command, 'log': str(output / name)})
with (output / name).open('w', encoding='utf-8') as log:
result = subprocess.run(command, cwd=ROOT, env=environment, stdout=log, stderr=subprocess.STDOUT)
commands[-1]['exitCode'] = result.returncode
return result.returncode
try:
if managed:
with socket.socket() as probe:
probe.bind(('127.0.0.1', 0))
port = probe.getsockname()[1]
environment['MARIADB_TEST_PORT'] = str(port)
environment['JOBTRACKER_MARIADB_TEST_ADMIN'] = test_connection(port)
code = run(compose + ['up', '-d', '--wait', '--wait-timeout', '120', 'mariadb-test'], 'compose-start.log')
else:
existing_connection(environment)
code = 0
if code == 0:
dotnet = environment.get('DOTNET_HOST_PATH') or shutil.which('dotnet')
if not dotnet: raise ValueError('Pinned .NET SDK is required; set DOTNET_HOST_PATH.')
project_file = 'JobTrackerApi.MariaDb.Tests/JobTrackerApi.MariaDb.Tests.csproj'
code = run([dotnet, 'restore', project_file, '--locked-mode'], 'restore.log')
if code == 0:
code = run([dotnet, 'test', project_file, '--configuration', 'Release', '--no-restore',
'--logger', 'trx;LogFileName=mariadb.trx', '--results-directory', str(output)], 'tests.log')
except Exception:
code = 1
raise
finally:
if managed:
# Never use down/-v: this generated project only owns its disposable test service.
run(compose + ['logs', '--no-color', 'mariadb-test'], 'server.log')
cleanup = run(compose + ['rm', '--stop', '--force', 'mariadb-test'], 'compose-cleanup.log')
if cleanup:
print('Test container cleanup failed; inspect compose-cleanup.log.')
if code == 0: code = cleanup
record = {'startedUtc':started, 'finishedUtc':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'mode':'compose' if managed else 'explicit-existing-loopback', 'expectedVersion':environment.get('JOBTRACKER_MARIADB_TEST_VERSION','12.2.2'),
'project':project if managed else None, 'exitCode':code, 'commands':commands}
(output / 'run.json').write_text(json.dumps(record, indent=2)+'\n', encoding='utf-8')
print(json.dumps({'exitCode':code, 'evidence':str(output), 'mode':record['mode']}))
return code
if __name__ == '__main__':
try: raise SystemExit(main())
except ValueError as error: raise SystemExit(str(error))
+50
View File
@@ -0,0 +1,50 @@
"""Offline guard tests for the isolated MariaDB lane."""
import importlib.util
from pathlib import Path
import unittest
from unittest import mock
import tempfile
import json
spec = importlib.util.spec_from_file_location('mariadb_integration', Path(__file__).with_name('mariadb-integration.py'))
runner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runner)
class Guards(unittest.TestCase):
def test_compose_project_is_generated_and_test_only(self):
command = runner.compose_command('jobtracker-it-abc123')
self.assertIn('jobtracker-it-abc123', command)
self.assertIn('docker-compose.database.yml', ' '.join(command))
self.assertNotIn('docker-compose.yml', command)
for name in ['production', 'jobtracker-local', '', 'jobtracker-it-x;whoami']:
with self.assertRaises(ValueError): runner.compose_command(name)
def test_credentials_and_address_are_local_test_only(self):
self.assertEqual(runner.test_connection(33307), 'Server=127.0.0.1;Port=33307;User ID=root;Password=jt-integration-only;SslMode=None')
for port in [0, 65536, -1]:
with self.assertRaises(ValueError): runner.test_connection(port)
def test_existing_server_requires_explicit_environment(self):
with self.assertRaises(ValueError): runner.existing_connection({})
with self.assertRaises(ValueError): runner.existing_connection({'JOBTRACKER_MARIADB_TEST_ADMIN':' '})
self.assertEqual(runner.existing_connection({'JOBTRACKER_MARIADB_TEST_ADMIN':'local-test'}), 'local-test')
def test_cleanup_failure_cannot_turn_into_a_green_ci_run(self):
with tempfile.TemporaryDirectory() as folder, mock.patch('sys.argv', ['runner','--output',folder]), \
mock.patch.dict(runner.os.environ, {'DOTNET_HOST_PATH':'test-dotnet'}, clear=True), \
mock.patch.object(runner.subprocess, 'run') as execute:
execute.side_effect = [mock.Mock(returncode=n) for n in [0,0,0,0,17]]
self.assertEqual(runner.main(), 17)
record=json.loads((Path(folder)/'run.json').read_text())
self.assertEqual(record['exitCode'],17)
self.assertEqual(record['commands'][-1]['command'][-4:], ['rm','--stop','--force','mariadb-test'])
self.assertNotIn('down',record['commands'][-1]['command'])
def test_setup_exception_is_recorded_as_failure(self):
with tempfile.TemporaryDirectory() as folder, mock.patch('sys.argv',['runner','--output',folder]), \
mock.patch.dict(runner.os.environ,{},clear=True), mock.patch.object(runner.shutil,'which',return_value=None), \
mock.patch.object(runner.subprocess,'run',return_value=mock.Mock(returncode=0)):
with self.assertRaises(ValueError): runner.main()
self.assertEqual(json.loads((Path(folder)/'run.json').read_text())['exitCode'],1)
if __name__ == '__main__': unittest.main()